Enable beta auto-deploy + Tianji beta ID / consent-gated events (#35) (#42)
Deploy Beta / unit-tests (push) Successful in 13s
Unit Tests / test (push) Successful in 12s
Deploy Beta / deploy-beta (push) Successful in 9s

## Summary

Closes #35.

- **Flip deploy workflows** (mirror `dta_webapp`): push/merge to `master` → auto-deploy **beta**; **Deploy Prod** is `workflow_dispatch` only
- **Tianji**: `REACT_APP_TIANJI_WEBSITE_ID` + `REACT_APP_DEPLOY_ENV` in `.env.production` / `.env.beta`; Tracker loads page views always on prod/beta
- **Consent policy**: page views always live; custom events / identify gated via `analyticsConsent` + `AnalyticsConsentBanner` + `trackEvent`/`identifyUser` helpers (call sites remain #36)
- Remove invalid Tianji `<script>` from `index.tsx` (Tracker component owns injection)
- README documents beta auto / prod button + Tianji policy

## Ops note — beta Tianji website ID

Prod ID unchanged: `cm7x7m52m03kbddswbswrt17y`.

`.env.beta` has `REACT_APP_TIANJI_WEBSITE_ID=` empty until a **Hesychia Beta** website is created in Tianji (workspace `cm7w8087y020lddswyhamadj2`). Paste the new id into `.env.beta` before beta page views show under a distinct site.

## Test plan

- [x] `npm run test:ci` (106 tests)
- [ ] Create Hesychia Beta Tianji website; set `REACT_APP_TIANJI_WEBSITE_ID` in `.env.beta`
- [ ] Merge to master → Deploy Beta runs; prod does **not** auto-deploy
- [ ] Manual Run workflow on Deploy Prod still works
- [ ] Beta build hits `beta.chatbackend` REST/WS; static root `/var/www/beta.chat.aimloperations/html` (8083)
- [ ] Prod/beta: tracker.js loads without accepting consent; banner shows until Accept/Decline; `trackEvent` only fires after AcceptReviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
2026-07-27 06:54:19 -07:00
parent 28a805404a
commit 966ad7aba4
17 changed files with 639 additions and 30 deletions
+77
View File
@@ -0,0 +1,77 @@
import { hasAnalyticsConsent, isAnalyticsEnvironment } from './analyticsConsent';
type QueuedCall =
| { type: 'track'; eventName: string; data?: Record<string, unknown> }
| { type: 'identify'; userInfo: Record<string, unknown> };
const queue: QueuedCall[] = [];
/** Named events for product analytics. Page views stay on automatic tracker.js. */
export const AnalyticsEvents = {
LOGIN_SUCCESS: 'Login Success',
LOGIN_FAILED: 'Login Failed',
SIGNUP_STARTED: 'Sign Up Started',
SIGNUP_SUCCESS: 'Sign Up Success',
SIGNUP_FAILED: 'Sign Up Failed',
LOGOUT: 'Logout',
CHECKOUT_STARTED: 'Checkout Started',
PAYMENT_SUCCESS: 'Payment Success',
PAYMENT_CANCELED: 'Payment Canceled',
CONVERSATION_CREATED: 'Conversation Created',
MESSAGE_SENT: 'Message Sent',
TOS_ACKNOWLEDGED: 'ToS Acknowledged',
BILLING_PORTAL_OPENED: 'Billing Portal Opened',
} as const;
export type AnalyticsEventName = (typeof AnalyticsEvents)[keyof typeof AnalyticsEvents];
/** Custom events + identify require consent. Automatic page views do not. */
const isEventTrackingEnabled = (): boolean =>
isAnalyticsEnvironment() && hasAnalyticsConsent();
const runQueuedCall = (call: QueuedCall): void => {
if (!window.tianji) return;
if (call.type === 'track') {
window.tianji.track(call.eventName, call.data);
return;
}
window.tianji.identify(call.userInfo);
};
export const flushAnalyticsQueue = (): void => {
if (!isEventTrackingEnabled() || !window.tianji) return;
while (queue.length > 0) {
const call = queue.shift();
if (call) runQueuedCall(call);
}
};
export const trackEvent = (
eventName: AnalyticsEventName | string,
data?: Record<string, unknown>,
): void => {
if (!isEventTrackingEnabled()) return;
const payload = data ? { ...data } : undefined;
if (window.tianji?.track) {
window.tianji.track(eventName, payload);
return;
}
queue.push({ type: 'track', eventName, data: payload });
};
export const identifyUser = (userInfo: Record<string, unknown>): void => {
if (!isEventTrackingEnabled()) return;
if (window.tianji?.identify) {
window.tianji.identify(userInfo);
return;
}
queue.push({ type: 'identify', userInfo: { ...userInfo } });
};