ad2app-lib 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ export declare const EVENTS: {
32
32
  readonly PROFILE_COMPLETED: "profile_completed";
33
33
  readonly LOGGED_IN: "logged_in";
34
34
  readonly SOCIAL_ACCOUNT_CONNECTED: "social_account_connected";
35
+ readonly SOCIAL_SIGN_IN_FAILED: "social_sign_in_failed";
35
36
  readonly COMPOSE_STARTED: "compose_started";
36
37
  readonly COMPOSE_STEP_VIEWED: "compose_step_viewed";
37
38
  readonly COMPOSE_BLOCKED: "compose_blocked";
@@ -44,6 +45,9 @@ export declare const EVENTS: {
44
45
  readonly CHECKOUT_STARTED: "checkout_started";
45
46
  readonly SUBSCRIPTION_STARTED: "subscription_started";
46
47
  readonly SUBSCRIPTION_CANCELED: "subscription_canceled";
48
+ readonly BILLING_PORTAL_OPENED: "billing_portal_opened";
49
+ readonly SUBSCRIPTION_REACTIVATED: "subscription_reactivated";
50
+ readonly CHECKOUT_CONFIRMATION_TIMED_OUT: "checkout_confirmation_timed_out";
47
51
  readonly PAYWALL_SHOWN: "paywall_shown";
48
52
  readonly PAYWALL_DISMISSED: "paywall_dismissed";
49
53
  readonly ACCESS_DENIED: "access_denied";
@@ -75,6 +79,12 @@ export declare const EVENTS: {
75
79
  readonly POST_RESCHEDULED: "post_rescheduled";
76
80
  readonly ACCOUNT_DISCONNECTED: "account_disconnected";
77
81
  readonly NOTIFICATION_PREFERENCES_UPDATED: "notification_preferences_updated";
82
+ readonly THEME_CHANGED: "theme_changed";
83
+ readonly FEEDBACK_SUBMITTED: "feedback_submitted";
84
+ readonly ACCOUNT_DELETED: "account_deleted";
85
+ readonly LANGUAGE_CHANGED: "language_changed";
86
+ readonly COMMENT_GUARD_MODE_CHANGED: "comment_guard_mode_changed";
87
+ readonly AI_TOOLS_REVOKE_REQUESTED: "ai_tools_revoke_requested";
78
88
  readonly NATIVE_SESSION_LAUNCHED: "native_session_launched";
79
89
  readonly NATIVE_SESSION_RENEWED: "native_session_renewed";
80
90
  readonly NATIVE_SESSION_ENDED: "native_session_ended";
@@ -165,7 +175,7 @@ export type ComposeStep = 'platforms' | 'content' | 'details' | 'review';
165
175
  * content, so neither may ride the event: only the class does. Same contract as
166
176
  * PublishFailureReason. Do NOT rename after shipping — wire values.
167
177
  */
168
- export type ComposeBlockReason = 'empty' | 'missing_fields' | 'over_limit' | 'media' | 'channel_blocked' | 'poll';
178
+ export type ComposeBlockReason = 'empty' | 'missing_fields' | 'over_limit' | 'media' | 'channel_blocked' | 'poll' | 'unconnected_platform';
169
179
  /**
170
180
  * Why a connect attempt ended without a connected account (spec 155 US3).
171
181
  *
@@ -196,9 +206,11 @@ export type ComposeBlockReason = 'empty' | 'missing_fields' | 'over_limit' | 'me
196
206
  export type OnboardingCompletedAt = 'connect' | 'install' | 'schedule';
197
207
  /** The finish lines, in the order the flow can reach them. */
198
208
  export declare const ONBOARDING_COMPLETED_AT: readonly ["connect", "install", "schedule"];
199
- export type ConnectBlockReason = 'paywall' | 'canceled' | 'failed' | 'returned_without_account' | 'personal_profile';
209
+ export type ConnectBlockReason = 'paywall' | 'canceled' | 'failed' | 'returned_without_account' | 'personal_profile' | 'api_failed' | 'timeout' | 'exception';
200
210
  /** The reason set, in the order the spec lists it. Iterable for contract tests and dashboards. */
201
- export declare const CONNECT_BLOCK_REASONS: readonly ["paywall", "canceled", "failed", "returned_without_account", "personal_profile"];
211
+ export declare const CONNECT_BLOCK_REASONS: readonly ["paywall", "canceled", "failed", "returned_without_account", "personal_profile", "api_failed", "timeout", "exception"];
212
+ export type SignInFailureReason = 'canceled' | 'missing_code' | 'invalid_provider' | 'no_email' | 'api_failed' | 'exception';
213
+ export declare const SIGN_IN_FAILURE_REASONS: readonly ["canceled", "missing_code", "invalid_provider", "no_email", "api_failed", "exception"];
202
214
  export type PublishFailureReason = 'auth' | 'rate_limit' | 'media' | 'content' | 'network' | 'platform' | 'unknown';
203
215
  /** Property shape per event. Keeps emitters honest across repos. */
204
216
  export interface EventProperties {
@@ -261,6 +273,10 @@ export interface EventProperties {
261
273
  account_count?: number;
262
274
  connection_ordinal?: number;
263
275
  };
276
+ [EVENTS.SOCIAL_SIGN_IN_FAILED]: {
277
+ platform?: string;
278
+ reason: SignInFailureReason;
279
+ };
264
280
  [EVENTS.COMPOSE_STARTED]: {
265
281
  source?: 'wizard' | 'quick';
266
282
  };
@@ -324,6 +340,15 @@ export interface EventProperties {
324
340
  plan: string;
325
341
  reason?: string;
326
342
  };
343
+ [EVENTS.BILLING_PORTAL_OPENED]: {
344
+ plan: string;
345
+ };
346
+ [EVENTS.SUBSCRIPTION_REACTIVATED]: {
347
+ plan: string;
348
+ };
349
+ [EVENTS.CHECKOUT_CONFIRMATION_TIMED_OUT]: {
350
+ had_session_id: boolean;
351
+ };
327
352
  [EVENTS.PAYWALL_SHOWN]: {
328
353
  surface: 'upgrade_modal' | 'pricing_page';
329
354
  trigger: 'user_click' | 'limit_reached' | 'connect_wall';
@@ -475,6 +500,22 @@ export interface EventProperties {
475
500
  enabled?: boolean;
476
501
  success: boolean;
477
502
  };
503
+ [EVENTS.THEME_CHANGED]: {
504
+ mode: 'light' | 'dark';
505
+ };
506
+ [EVENTS.FEEDBACK_SUBMITTED]: {
507
+ has_screenshot: boolean;
508
+ };
509
+ [EVENTS.ACCOUNT_DELETED]: {
510
+ tier: string;
511
+ };
512
+ [EVENTS.LANGUAGE_CHANGED]: {
513
+ language: 'en' | 'pl';
514
+ };
515
+ [EVENTS.COMMENT_GUARD_MODE_CHANGED]: {
516
+ mode: 'off' | 'flag-only' | 'auto-ignore';
517
+ };
518
+ [EVENTS.AI_TOOLS_REVOKE_REQUESTED]: Record<string, never>;
478
519
  [EVENTS.NATIVE_SESSION_LAUNCHED]: {
479
520
  outcome: 'restored' | 'none' | 'degraded';
480
521
  };
@@ -10,7 +10,7 @@
10
10
  * Do NOT rename events after they ship — historical data does not migrate.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.LAUNCH_CONTEXT_PROPS = exports.PERSON_PROPS = exports.CONNECT_BLOCK_REASONS = exports.ONBOARDING_COMPLETED_AT = exports.EMAIL_PROPS = exports.EVENTS = void 0;
13
+ exports.LAUNCH_CONTEXT_PROPS = exports.PERSON_PROPS = exports.SIGN_IN_FAILURE_REASONS = exports.CONNECT_BLOCK_REASONS = exports.ONBOARDING_COMPLETED_AT = exports.EMAIL_PROPS = exports.EVENTS = void 0;
14
14
  /** Canonical PostHog event names. */
15
15
  exports.EVENTS = {
16
16
  // Acquisition (landing)
@@ -42,6 +42,12 @@ exports.EVENTS = {
42
42
  PROFILE_COMPLETED: 'profile_completed', // the /complete-profile step (influencers)
43
43
  LOGGED_IN: 'logged_in',
44
44
  SOCIAL_ACCOUNT_CONNECTED: 'social_account_connected',
45
+ // 164 G007: OAuthCallbackPage's sign-in branch (state !== 'web-connect')
46
+ // had zero tracking on any of its 5 failure outcomes -- LOGGED_IN only
47
+ // ever fires on success (PostHogIdentify.tsx, centrally, off the redux
48
+ // authed state), so failures need their own event rather than an
49
+ // overloaded success/failure LOGGED_IN.
50
+ SOCIAL_SIGN_IN_FAILED: 'social_sign_in_failed',
45
51
  COMPOSE_STARTED: 'compose_started',
46
52
  // AD2-1371: the composer was a black box between COMPOSE_STARTED and
47
53
  // POST_SCHEDULED. Measured 2026-08-28: of seven users who connected an
@@ -61,6 +67,17 @@ exports.EVENTS = {
61
67
  CHECKOUT_STARTED: 'checkout_started',
62
68
  SUBSCRIPTION_STARTED: 'subscription_started',
63
69
  SUBSCRIPTION_CANCELED: 'subscription_canceled',
70
+ // 164 G009 (SubscriptionSection had zero tracking on either action): client
71
+ // intent only — CANCEL_INITIATED above is already backend/webhook-owned
72
+ // (fires off the real `cancel_at_period_end` transition), so cancel itself
73
+ // is deliberately NOT duplicated here.
74
+ BILLING_PORTAL_OPENED: 'billing_portal_opened',
75
+ SUBSCRIPTION_REACTIVATED: 'subscription_reactivated',
76
+ // 164 (ComposePage -> PaymentProcessingPage): the client gave up polling for
77
+ // Stripe/webhook confirmation. Distinct from the backend-owned PAYMENT_FAILED
78
+ // (a real decline) -- this only means "we could not confirm in time", which
79
+ // may still resolve moments later.
80
+ CHECKOUT_CONFIRMATION_TIMED_OUT: 'checkout_confirmation_timed_out',
64
81
  // Measurement baseline (080) — paywall / onboarding / exit-intent (frontend-owned)
65
82
  PAYWALL_SHOWN: 'paywall_shown', // any monetization surface is shown (upgrade modal / pricing page)
66
83
  PAYWALL_DISMISSED: 'paywall_dismissed', // closed without converting
@@ -110,6 +127,19 @@ exports.EVENTS = {
110
127
  ACCOUNT_DISCONNECTED: 'account_disconnected',
111
128
  // Settings.
112
129
  NOTIFICATION_PREFERENCES_UPDATED: 'notification_preferences_updated',
130
+ // 164 G009: SettingsPage had `track`/`EVENTS` imported but never called --
131
+ // theme, feedback, and account-deletion all fired nothing. NOTIFICATION_PREFERENCES_UPDATED
132
+ // above already covers the marketing-emails toggle (its declared `{preference, enabled,
133
+ // success}` shape fits exactly; reused rather than adding a parallel event).
134
+ THEME_CHANGED: 'theme_changed',
135
+ FEEDBACK_SUBMITTED: 'feedback_submitted',
136
+ ACCOUNT_DELETED: 'account_deleted',
137
+ // Shared LanguageSwitcher (SignIn/SignUp/Settings) had the same zero-tracking gap.
138
+ LANGUAGE_CHANGED: 'language_changed',
139
+ // Behind SHOW_COMMENT_GUARD_SECTION / SHOW_AI_TOOLS_SECTION (both default OFF) —
140
+ // no real user can reach either today, but fixed for completeness/G009.
141
+ COMMENT_GUARD_MODE_CHANGED: 'comment_guard_mode_changed',
142
+ AI_TOOLS_REVOKE_REQUESTED: 'ai_tools_revoke_requested',
113
143
  // Native session lifecycle (159) — the iOS wrapper's Keychain-backed session,
114
144
  // reported so "does it survive" is a query instead of an inference from login
115
145
  // counts. Native only; the web session's posture is unchanged (contract Rule 5).
@@ -151,6 +181,17 @@ exports.CONNECT_BLOCK_REASONS = [
151
181
  'failed',
152
182
  'returned_without_account',
153
183
  'personal_profile',
184
+ 'api_failed',
185
+ 'timeout',
186
+ 'exception',
187
+ ];
188
+ exports.SIGN_IN_FAILURE_REASONS = [
189
+ 'canceled',
190
+ 'missing_code',
191
+ 'invalid_provider',
192
+ 'no_email',
193
+ 'api_failed',
194
+ 'exception',
154
195
  ];
155
196
  /** Canonical person property keys (set via identify / $set). */
156
197
  exports.PERSON_PROPS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ad2app-lib",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "commonjs",
@@ -13,13 +13,19 @@ import { test } from 'node:test';
13
13
  import { CONNECT_BLOCK_REASONS, EVENTS } from './index';
14
14
  import type { ConnectBlockReason, EventProperties } from './index';
15
15
 
16
- test('the reason set is the five causes, as wire values', () => {
16
+ test('the reason set is the eight causes, as wire values', () => {
17
+ // 164 G001: widened from five to eight — 'api_failed'/'timeout'/'exception'
18
+ // cover connectAccount() itself failing before any redirect ever happens
19
+ // (confirmed live 2026-09-07 as the one previously-untracked branch).
17
20
  assert.deepEqual([...CONNECT_BLOCK_REASONS], [
18
21
  'paywall',
19
22
  'canceled',
20
23
  'failed',
21
24
  'returned_without_account',
22
25
  'personal_profile',
26
+ 'api_failed',
27
+ 'timeout',
28
+ 'exception',
23
29
  ]);
24
30
  });
25
31
 
@@ -90,6 +90,8 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
90
90
  // 'email' | 'google' union (compile-time, enforced by `tsc --noEmit`).
91
91
  [EVENTS.LOGGED_IN]: { method: "apple" },
92
92
  [EVENTS.SOCIAL_ACCOUNT_CONNECTED]: { platform: "instagram" },
93
+ // 164 G007
94
+ [EVENTS.SOCIAL_SIGN_IN_FAILED]: { platform: "facebook", reason: "canceled" },
93
95
  [EVENTS.COMPOSE_STARTED]: {},
94
96
  [EVENTS.COMPOSE_STEP_VIEWED]: { step: "content" },
95
97
  [EVENTS.COMPOSE_BLOCKED]: { step: "content", reasons: ["missing_fields"], reason_count: 1 },
@@ -102,6 +104,11 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
102
104
  [EVENTS.CHECKOUT_STARTED]: { plan: "starter" },
103
105
  [EVENTS.SUBSCRIPTION_STARTED]: { plan: "starter" },
104
106
  [EVENTS.SUBSCRIPTION_CANCELED]: { plan: "starter" },
107
+ // 164 G011
108
+ [EVENTS.BILLING_PORTAL_OPENED]: { plan: "pro" },
109
+ [EVENTS.SUBSCRIPTION_REACTIVATED]: { plan: "pro" },
110
+ // 164 (PaymentProcessingPage)
111
+ [EVENTS.CHECKOUT_CONFIRMATION_TIMED_OUT]: { had_session_id: true },
105
112
  // 080 measurement baseline
106
113
  [EVENTS.PAYWALL_SHOWN]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
107
114
  [EVENTS.PAYWALL_DISMISSED]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
@@ -139,6 +146,14 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
139
146
  [EVENTS.NATIVE_SESSION_LAUNCHED]: { outcome: "restored" },
140
147
  [EVENTS.NATIVE_SESSION_RENEWED]: {},
141
148
  [EVENTS.NATIVE_SESSION_ENDED]: { cause: "rejected" },
149
+ // 164 G009 — SettingsPage had track/EVENTS imported but never called
150
+ [EVENTS.THEME_CHANGED]: { mode: "dark" },
151
+ [EVENTS.FEEDBACK_SUBMITTED]: { has_screenshot: false },
152
+ [EVENTS.ACCOUNT_DELETED]: { tier: "pro" },
153
+ [EVENTS.LANGUAGE_CHANGED]: { language: "pl" },
154
+ // 164 G012 — both behind default-OFF flags, fixed for completeness
155
+ [EVENTS.COMMENT_GUARD_MODE_CHANGED]: { mode: "flag-only" },
156
+ [EVENTS.AI_TOOLS_REVOKE_REQUESTED]: {},
142
157
  };
143
158
 
144
159
  test("EVENTS values are 1:1 with EventProperties keys (no missing or typo'd event)", () => {
@@ -46,6 +46,12 @@ export const EVENTS = {
46
46
  PROFILE_COMPLETED: 'profile_completed', // the /complete-profile step (influencers)
47
47
  LOGGED_IN: 'logged_in',
48
48
  SOCIAL_ACCOUNT_CONNECTED: 'social_account_connected',
49
+ // 164 G007: OAuthCallbackPage's sign-in branch (state !== 'web-connect')
50
+ // had zero tracking on any of its 5 failure outcomes -- LOGGED_IN only
51
+ // ever fires on success (PostHogIdentify.tsx, centrally, off the redux
52
+ // authed state), so failures need their own event rather than an
53
+ // overloaded success/failure LOGGED_IN.
54
+ SOCIAL_SIGN_IN_FAILED: 'social_sign_in_failed',
49
55
  COMPOSE_STARTED: 'compose_started',
50
56
  // AD2-1371: the composer was a black box between COMPOSE_STARTED and
51
57
  // POST_SCHEDULED. Measured 2026-08-28: of seven users who connected an
@@ -66,6 +72,17 @@ export const EVENTS = {
66
72
  CHECKOUT_STARTED: 'checkout_started',
67
73
  SUBSCRIPTION_STARTED: 'subscription_started',
68
74
  SUBSCRIPTION_CANCELED: 'subscription_canceled',
75
+ // 164 G009 (SubscriptionSection had zero tracking on either action): client
76
+ // intent only — CANCEL_INITIATED above is already backend/webhook-owned
77
+ // (fires off the real `cancel_at_period_end` transition), so cancel itself
78
+ // is deliberately NOT duplicated here.
79
+ BILLING_PORTAL_OPENED: 'billing_portal_opened',
80
+ SUBSCRIPTION_REACTIVATED: 'subscription_reactivated',
81
+ // 164 (ComposePage -> PaymentProcessingPage): the client gave up polling for
82
+ // Stripe/webhook confirmation. Distinct from the backend-owned PAYMENT_FAILED
83
+ // (a real decline) -- this only means "we could not confirm in time", which
84
+ // may still resolve moments later.
85
+ CHECKOUT_CONFIRMATION_TIMED_OUT: 'checkout_confirmation_timed_out',
69
86
 
70
87
  // Measurement baseline (080) — paywall / onboarding / exit-intent (frontend-owned)
71
88
  PAYWALL_SHOWN: 'paywall_shown', // any monetization surface is shown (upgrade modal / pricing page)
@@ -121,6 +138,19 @@ export const EVENTS = {
121
138
 
122
139
  // Settings.
123
140
  NOTIFICATION_PREFERENCES_UPDATED: 'notification_preferences_updated',
141
+ // 164 G009: SettingsPage had `track`/`EVENTS` imported but never called --
142
+ // theme, feedback, and account-deletion all fired nothing. NOTIFICATION_PREFERENCES_UPDATED
143
+ // above already covers the marketing-emails toggle (its declared `{preference, enabled,
144
+ // success}` shape fits exactly; reused rather than adding a parallel event).
145
+ THEME_CHANGED: 'theme_changed',
146
+ FEEDBACK_SUBMITTED: 'feedback_submitted',
147
+ ACCOUNT_DELETED: 'account_deleted',
148
+ // Shared LanguageSwitcher (SignIn/SignUp/Settings) had the same zero-tracking gap.
149
+ LANGUAGE_CHANGED: 'language_changed',
150
+ // Behind SHOW_COMMENT_GUARD_SECTION / SHOW_AI_TOOLS_SECTION (both default OFF) —
151
+ // no real user can reach either today, but fixed for completeness/G009.
152
+ COMMENT_GUARD_MODE_CHANGED: 'comment_guard_mode_changed',
153
+ AI_TOOLS_REVOKE_REQUESTED: 'ai_tools_revoke_requested',
124
154
 
125
155
  // Native session lifecycle (159) — the iOS wrapper's Keychain-backed session,
126
156
  // reported so "does it survive" is a query instead of an inference from login
@@ -242,7 +272,12 @@ export type ComposeBlockReason =
242
272
  | 'over_limit' // caption longer than a selected platform allows
243
273
  | 'media' // media rejected (format, size, duration, count)
244
274
  | 'channel_blocked' // the channel itself refused (e.g. a failed prerequisite read)
245
- | 'poll'; // an incomplete or unpostable poll
275
+ | 'poll' // an incomplete or unpostable poll
276
+ // 164 G002: picking an unconnected platform at step 1 (Platforms) redirects
277
+ // straight to /accounts with zero analytics -- same "composer refused to
278
+ // let you proceed" shape as the publish-readiness reasons above, just one
279
+ // step earlier, so it reuses this event/reason set rather than a new event.
280
+ | 'unconnected_platform';
246
281
 
247
282
  /**
248
283
  * Why a connect attempt ended without a connected account (spec 155 US3).
@@ -285,7 +320,14 @@ export type ConnectBlockReason =
285
320
  | 'canceled'
286
321
  | 'failed'
287
322
  | 'returned_without_account'
288
- | 'personal_profile';
323
+ | 'personal_profile'
324
+ // 164 G001: 'failed' above is the POST-redirect case (backend appends
325
+ // ?connect=failed once the provider has already handed control back).
326
+ // These three cover the connectAccount() API call itself never getting a
327
+ // redirect at all — confirmed live 2026-09-07 as the one untracked branch.
328
+ | 'api_failed'
329
+ | 'timeout'
330
+ | 'exception';
289
331
 
290
332
  /** The reason set, in the order the spec lists it. Iterable for contract tests and dashboards. */
291
333
  export const CONNECT_BLOCK_REASONS = [
@@ -294,8 +336,31 @@ export const CONNECT_BLOCK_REASONS = [
294
336
  'failed',
295
337
  'returned_without_account',
296
338
  'personal_profile',
339
+ 'api_failed',
340
+ 'timeout',
341
+ 'exception',
297
342
  ] as const satisfies readonly ConnectBlockReason[];
298
343
 
344
+ // 164 G007: covers OAuthCallbackPage's social sign-in branch (as opposed to
345
+ // ConnectBlockReason, which covers its account-connect branch) -- distinct
346
+ // enums because the two flows fail for different reasons.
347
+ export type SignInFailureReason =
348
+ | 'canceled' // provider returned ?error= (user denied / backed out)
349
+ | 'missing_code' // callback landed with no ?code= at all
350
+ | 'invalid_provider' // :provider route param missing/unrecognized
351
+ | 'no_email' // provider granted no email (Facebook/TikTok phone-only) -- AD2-1252
352
+ | 'api_failed' // socialSignIn() resolved without an accessToken
353
+ | 'exception'; // socialSignIn() itself threw
354
+
355
+ export const SIGN_IN_FAILURE_REASONS = [
356
+ 'canceled',
357
+ 'missing_code',
358
+ 'invalid_provider',
359
+ 'no_email',
360
+ 'api_failed',
361
+ 'exception',
362
+ ] as const satisfies readonly SignInFailureReason[];
363
+
299
364
  export type PublishFailureReason =
300
365
  | 'auth' // token expired / revoked / missing scope / permission denied
301
366
  | 'rate_limit' // provider throttled us (429 / quota)
@@ -352,6 +417,7 @@ export interface EventProperties {
352
417
  // 080: 1 = first-ever account, 2 = second… — the "second action" activation hook.
353
418
  connection_ordinal?: number;
354
419
  };
420
+ [EVENTS.SOCIAL_SIGN_IN_FAILED]: { platform?: string; reason: SignInFailureReason };
355
421
  [EVENTS.COMPOSE_STARTED]: { source?: 'wizard' | 'quick' };
356
422
  [EVENTS.COMPOSE_STEP_VIEWED]: { step: ComposeStep };
357
423
  [EVENTS.COMPOSE_BLOCKED]: {
@@ -419,6 +485,9 @@ export interface EventProperties {
419
485
  trigger_source?: TriggerSource; // 080
420
486
  };
421
487
  [EVENTS.SUBSCRIPTION_CANCELED]: { plan: string; reason?: string };
488
+ [EVENTS.BILLING_PORTAL_OPENED]: { plan: string };
489
+ [EVENTS.SUBSCRIPTION_REACTIVATED]: { plan: string };
490
+ [EVENTS.CHECKOUT_CONFIRMATION_TIMED_OUT]: { had_session_id: boolean };
422
491
 
423
492
  // ── Measurement baseline (080) — new event property shapes ──────────────────
424
493
  // Paywall surfaces (frontend). hit_number is the analytical truth (an event
@@ -511,6 +580,12 @@ export interface EventProperties {
511
580
 
512
581
  // Settings — which preference toggled and its new state.
513
582
  [EVENTS.NOTIFICATION_PREFERENCES_UPDATED]: { preference?: string; enabled?: boolean; success: boolean };
583
+ [EVENTS.THEME_CHANGED]: { mode: 'light' | 'dark' };
584
+ [EVENTS.FEEDBACK_SUBMITTED]: { has_screenshot: boolean };
585
+ [EVENTS.ACCOUNT_DELETED]: { tier: string };
586
+ [EVENTS.LANGUAGE_CHANGED]: { language: 'en' | 'pl' };
587
+ [EVENTS.COMMENT_GUARD_MODE_CHANGED]: { mode: 'off' | 'flag-only' | 'auto-ignore' };
588
+ [EVENTS.AI_TOOLS_REVOKE_REQUESTED]: Record<string, never>;
514
589
 
515
590
  // Native session lifecycle (159, contract session-reports.md). `outcome`
516
591
  // covers all three launch reports in one event (restored / none / degraded)