@metamask-previews/analytics-controller 2.0.0-preview-fe4b3be11 → 2.1.0-preview-2f57247

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +46 -5
  3. package/dist/AnalyticsController-method-action-types.cjs.map +1 -1
  4. package/dist/AnalyticsController-method-action-types.d.cts +106 -2
  5. package/dist/AnalyticsController-method-action-types.d.cts.map +1 -1
  6. package/dist/AnalyticsController-method-action-types.d.mts +106 -2
  7. package/dist/AnalyticsController-method-action-types.d.mts.map +1 -1
  8. package/dist/AnalyticsController-method-action-types.mjs.map +1 -1
  9. package/dist/AnalyticsController.cjs +364 -14
  10. package/dist/AnalyticsController.cjs.map +1 -1
  11. package/dist/AnalyticsController.d.cts +121 -2
  12. package/dist/AnalyticsController.d.cts.map +1 -1
  13. package/dist/AnalyticsController.d.mts +121 -2
  14. package/dist/AnalyticsController.d.mts.map +1 -1
  15. package/dist/AnalyticsController.mjs +363 -13
  16. package/dist/AnalyticsController.mjs.map +1 -1
  17. package/dist/EventFragment.types.cjs +3 -0
  18. package/dist/EventFragment.types.cjs.map +1 -0
  19. package/dist/EventFragment.types.d.cts +111 -0
  20. package/dist/EventFragment.types.d.cts.map +1 -0
  21. package/dist/EventFragment.types.d.mts +111 -0
  22. package/dist/EventFragment.types.d.mts.map +1 -0
  23. package/dist/EventFragment.types.mjs +2 -0
  24. package/dist/EventFragment.types.mjs.map +1 -0
  25. package/dist/index.cjs +2 -1
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +3 -2
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.mts +3 -2
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +1 -1
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/selectors.cjs +19 -0
  34. package/dist/selectors.cjs.map +1 -1
  35. package/dist/selectors.d.cts +3 -0
  36. package/dist/selectors.d.cts.map +1 -1
  37. package/dist/selectors.d.mts +3 -0
  38. package/dist/selectors.d.mts.map +1 -1
  39. package/dist/selectors.mjs +19 -0
  40. package/dist/selectors.mjs.map +1 -1
  41. package/package.json +2 -2
@@ -10,9 +10,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _AnalyticsController_instances, _AnalyticsController_platformAdapter, _AnalyticsController_isAnonymousEventsFeatureEnabled, _AnalyticsController_isEventQueuePersistenceEnabled, _AnalyticsController_isPreConsentQueueEnabled, _AnalyticsController_isGeolocationEnabled, _AnalyticsController_initPromise, _AnalyticsController_locationResolvePromise, _AnalyticsController_locationContext, _AnalyticsController_performInit, _AnalyticsController_maybeResolveLocation, _AnalyticsController_resolveLocationContext, _AnalyticsController_withLocationContext, _AnalyticsController_sendOrQueueTrackEvent, _AnalyticsController_sendOrQueueIdentifyEvent, _AnalyticsController_sendOrQueueViewEvent, _AnalyticsController_enqueueEvent, _AnalyticsController_sendQueuedEvent, _AnalyticsController_replayQueuedEvents, _AnalyticsController_removeQueuedEvent, _AnalyticsController_clearQueuedEvents, _AnalyticsController_enqueuePreConsentEvent, _AnalyticsController_replayPreConsentEvents, _AnalyticsController_enrichPreConsentEvent, _AnalyticsController_clearPreConsentEvents, _AnalyticsController_reconcilePreConsentEvents;
13
+ var _AnalyticsController_instances, _AnalyticsController_platformAdapter, _AnalyticsController_isAnonymousEventsFeatureEnabled, _AnalyticsController_isEventQueuePersistenceEnabled, _AnalyticsController_isPreConsentQueueEnabled, _AnalyticsController_isGeolocationEnabled, _AnalyticsController_isEventFragmentsEnabled, _AnalyticsController_initPromise, _AnalyticsController_locationResolvePromise, _AnalyticsController_locationContext, _AnalyticsController_performInit, _AnalyticsController_maybeResolveLocation, _AnalyticsController_resolveLocationContext, _AnalyticsController_withLocationContext, _AnalyticsController_sendOrQueueTrackEvent, _AnalyticsController_sendOrQueueIdentifyEvent, _AnalyticsController_sendOrQueueViewEvent, _AnalyticsController_enqueueEvent, _AnalyticsController_sendQueuedEvent, _AnalyticsController_replayQueuedEvents, _AnalyticsController_removeQueuedEvent, _AnalyticsController_clearQueuedEvents, _AnalyticsController_enqueuePreConsentEvent, _AnalyticsController_replayPreConsentEvents, _AnalyticsController_enrichPreConsentEvent, _AnalyticsController_clearPreConsentEvents, _AnalyticsController_reconcilePreConsentEvents, _AnalyticsController_reconcileEventFragments, _AnalyticsController_purgeStaleEventFragments, _AnalyticsController_getEventFragment, _AnalyticsController_setEventFragment, _AnalyticsController_removeEventFragment, _AnalyticsController_clearEventFragments, _AnalyticsController_shouldIgnoreEventFragmentCall, _AnalyticsController_emitEventFragment, _AnalyticsController_isAnalyticsCaptureAllowed;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.AnalyticsController = exports.getDefaultAnalyticsControllerState = exports.controllerName = void 0;
15
+ exports.AnalyticsController = exports.getDefaultAnalyticsControllerState = exports.EVENT_FRAGMENT_MAX_AGE = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
17
  const lodash_1 = require("lodash");
18
18
  const uuid_1 = require("uuid");
@@ -26,6 +26,16 @@ const selectors_js_1 = require("./selectors.cjs");
26
26
  * when composed with other controllers.
27
27
  */
28
28
  exports.controllerName = 'AnalyticsController';
29
+ /**
30
+ * Maximum age of a persisted event fragment, measured from
31
+ * {@link AnalyticsEventFragment.lastUpdated}.
32
+ *
33
+ * Fragments older than this are discarded during {@link AnalyticsController.init}
34
+ * without emitting a success or failure event. Confirmation journeys that span a
35
+ * restart are expected to resume within this window; abandoned ones must not keep
36
+ * `properties` or `sensitiveProperties` in storage indefinitely.
37
+ */
38
+ exports.EVENT_FRAGMENT_MAX_AGE = 24 * 60 * 60 * 1000;
29
39
  /**
30
40
  * Returns default values for AnalyticsController state.
31
41
  *
@@ -78,6 +88,12 @@ const analyticsControllerMetadata = {
78
88
  includeInDebugSnapshot: false,
79
89
  usedInUi: false,
80
90
  },
91
+ eventFragments: {
92
+ includeInStateLogs: false,
93
+ persist: true,
94
+ includeInDebugSnapshot: false,
95
+ usedInUi: false,
96
+ },
81
97
  };
82
98
  // === MESSENGER ===
83
99
  const MESSENGER_EXPOSED_METHODS = [
@@ -87,6 +103,12 @@ const MESSENGER_EXPOSED_METHODS = [
87
103
  'optIn',
88
104
  'optOut',
89
105
  'resetConsentDecision',
106
+ 'createEventFragment',
107
+ 'upsertEventFragment',
108
+ 'updateEventFragment',
109
+ 'getEventFragmentById',
110
+ 'deleteEventFragment',
111
+ 'finalizeEventFragment',
90
112
  ];
91
113
  /**
92
114
  * Returns whether a value is a non-array object.
@@ -158,6 +180,69 @@ function isAnalyticsQueuedEvent(value) {
158
180
  }
159
181
  return false;
160
182
  }
183
+ /**
184
+ * Returns whether a value is a valid persisted event fragment.
185
+ *
186
+ * @param value - The value to check.
187
+ * @returns True if the value is an event fragment.
188
+ */
189
+ function isAnalyticsEventFragment(value) {
190
+ if (!isRecord(value)) {
191
+ return false;
192
+ }
193
+ return (typeof value.id === 'string' &&
194
+ typeof value.createdAt === 'number' &&
195
+ typeof value.lastUpdated === 'number' &&
196
+ isRecord(value.properties) &&
197
+ isRecord(value.sensitiveProperties) &&
198
+ (value.initialEvent === undefined ||
199
+ typeof value.initialEvent === 'string') &&
200
+ (value.successEvent === undefined ||
201
+ typeof value.successEvent === 'string') &&
202
+ (value.failureEvent === undefined ||
203
+ typeof value.failureEvent === 'string') &&
204
+ (value.context === undefined || isRecord(value.context)) &&
205
+ (value.persist === undefined || typeof value.persist === 'boolean'));
206
+ }
207
+ /**
208
+ * Merges a payload into an event fragment.
209
+ *
210
+ * `properties`, `sensitiveProperties` and `context` are merged one level deep,
211
+ * so a key written twice is replaced rather than combined. This keeps array
212
+ * values predictable: writing a shorter array replaces the longer one instead
213
+ * of leaving stale trailing entries behind.
214
+ *
215
+ * @param fragment - The fragment to merge into.
216
+ * @param payload - The payload to merge.
217
+ * @returns A new fragment with the payload applied.
218
+ */
219
+ function mergeEventFragment(fragment, payload) {
220
+ const context = mergeEventFragmentContext(fragment.context, payload.context);
221
+ return {
222
+ ...fragment,
223
+ properties: { ...fragment.properties, ...(payload.properties ?? {}) },
224
+ sensitiveProperties: {
225
+ ...fragment.sensitiveProperties,
226
+ ...(payload.sensitiveProperties ?? {}),
227
+ },
228
+ ...(context === undefined ? {} : { context }),
229
+ lastUpdated: Date.now(),
230
+ };
231
+ }
232
+ /**
233
+ * Merges two optional analytics contexts, preserving `undefined` when neither
234
+ * side has one so an empty context is never sent.
235
+ *
236
+ * @param base - The context to merge into.
237
+ * @param override - The context whose fields win.
238
+ * @returns The merged context, or `undefined` when both sides are unset.
239
+ */
240
+ function mergeEventFragmentContext(base, override) {
241
+ if (base === undefined && override === undefined) {
242
+ return undefined;
243
+ }
244
+ return { ...(base ?? {}), ...(override ?? {}) };
245
+ }
161
246
  /**
162
247
  * The AnalyticsController manages analytics tracking across platforms (Mobile/Extension).
163
248
  * It provides a unified interface for tracking events, identifying users, and managing
@@ -184,10 +269,11 @@ class AnalyticsController extends base_controller_1.BaseController {
184
269
  * @param options.isEventQueuePersistenceEnabled - Whether analytics event queue persistence is enabled
185
270
  * @param options.isPreConsentQueueEnabled - Whether the pre-consent event queue is enabled
186
271
  * @param options.isGeolocationEnabled - Whether geolocation enrichment is enabled
272
+ * @param options.isEventFragmentsEnabled - Whether the event fragments feature is enabled
187
273
  * @throws Error if state.analyticsId is missing or not a valid UUIDv4
188
274
  * @remarks After construction, call {@link AnalyticsController.init} to complete initialization.
189
275
  */
190
- constructor({ state, messenger, platformAdapter, isAnonymousEventsFeatureEnabled = false, isEventQueuePersistenceEnabled = false, isPreConsentQueueEnabled = false, isGeolocationEnabled = false, }) {
276
+ constructor({ state, messenger, platformAdapter, isAnonymousEventsFeatureEnabled = false, isEventQueuePersistenceEnabled = false, isPreConsentQueueEnabled = false, isGeolocationEnabled = false, isEventFragmentsEnabled = false, }) {
191
277
  const initialState = {
192
278
  ...getDefaultAnalyticsControllerState(),
193
279
  ...state,
@@ -205,6 +291,7 @@ class AnalyticsController extends base_controller_1.BaseController {
205
291
  _AnalyticsController_isEventQueuePersistenceEnabled.set(this, void 0);
206
292
  _AnalyticsController_isPreConsentQueueEnabled.set(this, void 0);
207
293
  _AnalyticsController_isGeolocationEnabled.set(this, void 0);
294
+ _AnalyticsController_isEventFragmentsEnabled.set(this, void 0);
208
295
  /**
209
296
  * The in-flight (or settled) initialization promise. Set on the first
210
297
  * {@link init} call and returned by subsequent calls so overlapping callers
@@ -221,6 +308,7 @@ class AnalyticsController extends base_controller_1.BaseController {
221
308
  __classPrivateFieldSet(this, _AnalyticsController_isEventQueuePersistenceEnabled, isEventQueuePersistenceEnabled, "f");
222
309
  __classPrivateFieldSet(this, _AnalyticsController_isPreConsentQueueEnabled, isPreConsentQueueEnabled, "f");
223
310
  __classPrivateFieldSet(this, _AnalyticsController_isGeolocationEnabled, isGeolocationEnabled, "f");
311
+ __classPrivateFieldSet(this, _AnalyticsController_isEventFragmentsEnabled, isEventFragmentsEnabled, "f");
224
312
  __classPrivateFieldSet(this, _AnalyticsController_platformAdapter, platformAdapter, "f");
225
313
  __classPrivateFieldSet(this, _AnalyticsController_initPromise, undefined, "f");
226
314
  __classPrivateFieldSet(this, _AnalyticsController_locationResolvePromise, undefined, "f");
@@ -233,6 +321,7 @@ class AnalyticsController extends base_controller_1.BaseController {
233
321
  eventQueuePersistenceEnabled: __classPrivateFieldGet(this, _AnalyticsController_isEventQueuePersistenceEnabled, "f"),
234
322
  preConsentQueueEnabled: __classPrivateFieldGet(this, _AnalyticsController_isPreConsentQueueEnabled, "f"),
235
323
  geolocationEnabled: __classPrivateFieldGet(this, _AnalyticsController_isGeolocationEnabled, "f"),
324
+ eventFragmentsEnabled: __classPrivateFieldGet(this, _AnalyticsController_isEventFragmentsEnabled, "f"),
236
325
  });
237
326
  }
238
327
  /**
@@ -270,15 +359,11 @@ class AnalyticsController extends base_controller_1.BaseController {
270
359
  * @param context - Optional platform-specific context forwarded to the platform adapter.
271
360
  */
272
361
  trackEvent(event, context) {
273
- if (!selectors_js_1.analyticsControllerSelectors.selectEnabled(this.state)) {
274
- // While the user is undecided, fall through so the event is processed and
275
- // captured in the pre-consent queue (see #sendOrQueueTrackEvent) to be
276
- // replayed if they later opt in. Otherwise (opted out, or pre-consent
277
- // queue disabled) drop it.
278
- const shouldQueuePreConsent = __classPrivateFieldGet(this, _AnalyticsController_isPreConsentQueueEnabled, "f") && !this.state.consentDecisionMade;
279
- if (!shouldQueuePreConsent) {
280
- return;
281
- }
362
+ // An event captured while the user is still undecided is held in the
363
+ // pre-consent queue (see #sendOrQueueTrackEvent) instead of being
364
+ // delivered, and replayed if they later opt in.
365
+ if (!__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_isAnalyticsCaptureAllowed).call(this)) {
366
+ return;
282
367
  }
283
368
  // if event does not have properties, send event without properties
284
369
  // and return to prevent any additional processing
@@ -336,6 +421,160 @@ class AnalyticsController extends base_controller_1.BaseController {
336
421
  // Delegate to platform adapter
337
422
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_sendOrQueueViewEvent).call(this, name, properties, __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_withLocationContext).call(this, context));
338
423
  }
424
+ /**
425
+ * Create an event fragment.
426
+ *
427
+ * A fragment accumulates properties across a user journey so that several
428
+ * parts of a client can contribute to the same set of events without
429
+ * re-deriving them. Declaring `successEvent` and `failureEvent` turns the
430
+ * fragment into a funnel that {@link finalizeEventFragment} closes. Declaring
431
+ * none of the event names makes it a pure property bag that the client reads
432
+ * back with {@link getEventFragmentById} when it emits its own events.
433
+ *
434
+ * Any existing fragment with the same ID is replaced, so a new journey never
435
+ * inherits properties from a stale one.
436
+ *
437
+ * Nothing is created unless the user is opted in, or undecided with the
438
+ * pre-consent queue enabled, so an opted-out user accumulates no fragment
439
+ * data.
440
+ *
441
+ * @param options - The fragment definition. An ID is generated when one is
442
+ * not supplied.
443
+ * @returns A read-only copy of the created fragment, or `undefined` when the
444
+ * event fragments feature is disabled or the consent state does not allow
445
+ * capture. Mutating the returned object does not change controller state.
446
+ * Use {@link updateEventFragment} or {@link upsertEventFragment} to write.
447
+ */
448
+ createEventFragment(options = {}) {
449
+ if (__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_shouldIgnoreEventFragmentCall).call(this, 'createEventFragment')) {
450
+ return undefined;
451
+ }
452
+ const now = Date.now();
453
+ const fragment = {
454
+ id: options.id ?? (0, uuid_1.v4)(),
455
+ properties: { ...(options.properties ?? {}) },
456
+ sensitiveProperties: { ...(options.sensitiveProperties ?? {}) },
457
+ createdAt: now,
458
+ lastUpdated: now,
459
+ ...(options.initialEvent === undefined
460
+ ? {}
461
+ : { initialEvent: options.initialEvent }),
462
+ ...(options.successEvent === undefined
463
+ ? {}
464
+ : { successEvent: options.successEvent }),
465
+ ...(options.failureEvent === undefined
466
+ ? {}
467
+ : { failureEvent: options.failureEvent }),
468
+ ...(options.context === undefined
469
+ ? {}
470
+ : { context: { ...options.context } }),
471
+ ...(options.persist === undefined ? {} : { persist: options.persist }),
472
+ };
473
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_setEventFragment).call(this, fragment);
474
+ if (fragment.initialEvent) {
475
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_emitEventFragment).call(this, fragment, fragment.initialEvent, fragment.context);
476
+ }
477
+ return (0, lodash_1.cloneDeep)(fragment);
478
+ }
479
+ /**
480
+ * Write to an event fragment, creating a property bag if none exists.
481
+ *
482
+ * This is the ergonomic entry point for contributors that do not know
483
+ * whether the journey has been started yet, and it avoids the read then
484
+ * write race a caller would otherwise have to implement itself.
485
+ *
486
+ * @param id - The fragment ID.
487
+ * @param payload - The properties and context to merge in.
488
+ */
489
+ upsertEventFragment(id, payload = {}) {
490
+ if (__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_shouldIgnoreEventFragmentCall).call(this, 'upsertEventFragment')) {
491
+ return;
492
+ }
493
+ const fragment = __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_getEventFragment).call(this, id);
494
+ if (!fragment) {
495
+ this.createEventFragment({ id, ...payload });
496
+ return;
497
+ }
498
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_setEventFragment).call(this, mergeEventFragment(fragment, payload));
499
+ }
500
+ /**
501
+ * Write to an existing event fragment.
502
+ *
503
+ * @param id - The fragment ID.
504
+ * @param payload - The properties and context to merge in.
505
+ * @throws Error if no fragment has that ID when the call is not ignored.
506
+ * Use {@link upsertEventFragment} when the fragment may not exist yet.
507
+ * When the event fragments feature is disabled or the consent state does not
508
+ * allow capture, the call is a logged no-op and does not throw.
509
+ */
510
+ updateEventFragment(id, payload = {}) {
511
+ if (__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_shouldIgnoreEventFragmentCall).call(this, 'updateEventFragment')) {
512
+ return;
513
+ }
514
+ const fragment = __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_getEventFragment).call(this, id);
515
+ if (!fragment) {
516
+ throw new Error(`Event fragment with id ${id} does not exist.`);
517
+ }
518
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_setEventFragment).call(this, mergeEventFragment(fragment, payload));
519
+ }
520
+ /**
521
+ * Read an event fragment.
522
+ *
523
+ * @param id - The fragment ID.
524
+ * @returns A read-only copy of the fragment, or `undefined` when no fragment
525
+ * has that ID, the event fragments feature is disabled, or the consent state
526
+ * does not allow capture. Mutating the returned object does not change
527
+ * controller state. Use {@link updateEventFragment} or
528
+ * {@link upsertEventFragment} to write.
529
+ */
530
+ getEventFragmentById(id) {
531
+ if (__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_shouldIgnoreEventFragmentCall).call(this, 'getEventFragmentById')) {
532
+ return undefined;
533
+ }
534
+ const fragment = __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_getEventFragment).call(this, id);
535
+ return fragment === undefined ? undefined : (0, lodash_1.cloneDeep)(fragment);
536
+ }
537
+ /**
538
+ * Discard an event fragment without emitting anything.
539
+ *
540
+ * @param id - The fragment ID.
541
+ */
542
+ deleteEventFragment(id) {
543
+ if (__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_shouldIgnoreEventFragmentCall).call(this, 'deleteEventFragment')) {
544
+ return;
545
+ }
546
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_removeEventFragment).call(this, id);
547
+ }
548
+ /**
549
+ * Close an event fragment, emitting its closing event and discarding it.
550
+ *
551
+ * The event emitted is `failureEvent` when the journey was abandoned and
552
+ * `successEvent` otherwise. A fragment that does not declare the relevant
553
+ * event name is discarded silently, which is what makes a pure property bag
554
+ * possible.
555
+ *
556
+ * @param id - The fragment ID.
557
+ * @param options - Finalization options.
558
+ * @param options.abandoned - Whether the journey was abandoned.
559
+ * @param options.context - Context merged over the fragment's own context.
560
+ * @throws Error if no fragment has that ID when the call is not ignored.
561
+ * When the event fragments feature is disabled or the consent state does not
562
+ * allow capture, the call is a logged no-op and does not throw.
563
+ */
564
+ finalizeEventFragment(id, { abandoned = false, context } = {}) {
565
+ if (__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_shouldIgnoreEventFragmentCall).call(this, 'finalizeEventFragment')) {
566
+ return;
567
+ }
568
+ const fragment = __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_getEventFragment).call(this, id);
569
+ if (!fragment) {
570
+ throw new Error(`Event fragment with id ${id} does not exist.`);
571
+ }
572
+ const eventName = abandoned ? fragment.failureEvent : fragment.successEvent;
573
+ if (eventName) {
574
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_emitEventFragment).call(this, fragment, eventName, mergeEventFragmentContext(fragment.context, context));
575
+ }
576
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_removeEventFragment).call(this, id);
577
+ }
339
578
  /**
340
579
  * Opt in to analytics.
341
580
  *
@@ -366,7 +605,8 @@ class AnalyticsController extends base_controller_1.BaseController {
366
605
  * Opt out of analytics.
367
606
  *
368
607
  * Records that a consent decision has been made and discards any persisted
369
- * events so nothing captured before the decision is ever delivered.
608
+ * events and in-progress event fragments so nothing captured before the
609
+ * decision is ever delivered.
370
610
  */
371
611
  optOut() {
372
612
  this.update((state) => {
@@ -375,6 +615,7 @@ class AnalyticsController extends base_controller_1.BaseController {
375
615
  });
376
616
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearQueuedEvents).call(this);
377
617
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearPreConsentEvents).call(this);
618
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearEventFragments).call(this);
378
619
  }
379
620
  /**
380
621
  * Reset the consent decision back to undecided.
@@ -383,6 +624,10 @@ class AnalyticsController extends base_controller_1.BaseController {
383
624
  * preference and discards the delivery queue, but preserves any pre-consent
384
625
  * events so they can still be replayed if the user opts in again. The user is
385
626
  * treated as undecided again.
627
+ *
628
+ * In-progress event fragments are kept only while the undecided user can
629
+ * still accumulate them, and discarded otherwise, so no fragment outlives the
630
+ * consent state that allowed it.
386
631
  */
387
632
  resetConsentDecision() {
388
633
  this.update((state) => {
@@ -390,16 +635,30 @@ class AnalyticsController extends base_controller_1.BaseController {
390
635
  state.consentDecisionMade = false;
391
636
  });
392
637
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearQueuedEvents).call(this);
638
+ if (!__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_isAnalyticsCaptureAllowed).call(this)) {
639
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearEventFragments).call(this);
640
+ }
393
641
  }
394
642
  }
395
643
  exports.AnalyticsController = AnalyticsController;
396
- _AnalyticsController_platformAdapter = new WeakMap(), _AnalyticsController_isAnonymousEventsFeatureEnabled = new WeakMap(), _AnalyticsController_isEventQueuePersistenceEnabled = new WeakMap(), _AnalyticsController_isPreConsentQueueEnabled = new WeakMap(), _AnalyticsController_isGeolocationEnabled = new WeakMap(), _AnalyticsController_initPromise = new WeakMap(), _AnalyticsController_locationResolvePromise = new WeakMap(), _AnalyticsController_locationContext = new WeakMap(), _AnalyticsController_instances = new WeakSet(), _AnalyticsController_performInit =
644
+ _AnalyticsController_platformAdapter = new WeakMap(), _AnalyticsController_isAnonymousEventsFeatureEnabled = new WeakMap(), _AnalyticsController_isEventQueuePersistenceEnabled = new WeakMap(), _AnalyticsController_isPreConsentQueueEnabled = new WeakMap(), _AnalyticsController_isGeolocationEnabled = new WeakMap(), _AnalyticsController_isEventFragmentsEnabled = new WeakMap(), _AnalyticsController_initPromise = new WeakMap(), _AnalyticsController_locationResolvePromise = new WeakMap(), _AnalyticsController_locationContext = new WeakMap(), _AnalyticsController_instances = new WeakSet(), _AnalyticsController_performInit =
397
645
  /**
398
646
  * Performs the one-time initialization work: resolve geolocation, run the
399
647
  * platform adapter's onSetupCompleted lifecycle hook, then replay any queued
400
648
  * and pre-consent events.
401
649
  */
402
650
  async function _AnalyticsController_performInit() {
651
+ // Snapshot fragment IDs and createdAt before any awaited init work so
652
+ // reconciliation can tell previous-session leftovers from fragments
653
+ // created or replaced while init runs.
654
+ const initEventFragmentSnapshot = new Map();
655
+ for (const [id, fragment] of Object.entries(this.state.eventFragments ?? {})) {
656
+ if (isAnalyticsEventFragment(fragment) &&
657
+ fragment.id === id &&
658
+ typeof fragment.createdAt === 'number') {
659
+ initEventFragmentSnapshot.set(id, fragment.createdAt);
660
+ }
661
+ }
403
662
  // Resolve geolocation only when the user is already opted in; for undecided
404
663
  // or opted-out users it is deferred to {@link optIn}. Awaited so that an
405
664
  // already-opted-in session has location available before events replay.
@@ -415,6 +674,7 @@ async function _AnalyticsController_performInit() {
415
674
  }
416
675
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_replayQueuedEvents).call(this);
417
676
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_reconcilePreConsentEvents).call(this);
677
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_reconcileEventFragments).call(this, initEventFragmentSnapshot);
418
678
  }, _AnalyticsController_maybeResolveLocation = function _AnalyticsController_maybeResolveLocation() {
419
679
  if (__classPrivateFieldGet(this, _AnalyticsController_isGeolocationEnabled, "f") &&
420
680
  __classPrivateFieldGet(this, _AnalyticsController_locationResolvePromise, "f") === undefined &&
@@ -637,5 +897,95 @@ async function _AnalyticsController_performInit() {
637
897
  else if (this.state.consentDecisionMade) {
638
898
  __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearPreConsentEvents).call(this);
639
899
  }
900
+ }, _AnalyticsController_reconcileEventFragments = function _AnalyticsController_reconcileEventFragments(initEventFragmentSnapshot) {
901
+ const fragments = this.state.eventFragments;
902
+ if (!fragments) {
903
+ return;
904
+ }
905
+ if (!__classPrivateFieldGet(this, _AnalyticsController_isEventFragmentsEnabled, "f") || !__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_isAnalyticsCaptureAllowed).call(this)) {
906
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_clearEventFragments).call(this);
907
+ return;
908
+ }
909
+ __classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_purgeStaleEventFragments).call(this, fragments, initEventFragmentSnapshot);
910
+ }, _AnalyticsController_purgeStaleEventFragments = function _AnalyticsController_purgeStaleEventFragments(currentEventFragments, initEventFragmentSnapshot) {
911
+ const eventFragments = {};
912
+ const now = Date.now();
913
+ for (const [id, fragment] of Object.entries(currentEventFragments)) {
914
+ if (!isAnalyticsEventFragment(fragment) || fragment.id !== id) {
915
+ (0, AnalyticsLogger_js_1.projectLogger)('Dropping invalid persisted event fragment', { id });
916
+ continue;
917
+ }
918
+ if (now - fragment.lastUpdated > exports.EVENT_FRAGMENT_MAX_AGE) {
919
+ (0, AnalyticsLogger_js_1.projectLogger)('Dropping expired persisted event fragment', { id });
920
+ continue;
921
+ }
922
+ const snapshotCreatedAt = initEventFragmentSnapshot.get(id);
923
+ if (fragment.persist === true ||
924
+ snapshotCreatedAt === undefined ||
925
+ fragment.createdAt !== snapshotCreatedAt) {
926
+ eventFragments[id] = fragment;
927
+ }
928
+ }
929
+ if (Object.keys(eventFragments).length ===
930
+ Object.keys(currentEventFragments).length) {
931
+ return;
932
+ }
933
+ this.update((state) => {
934
+ state.eventFragments = eventFragments;
935
+ });
936
+ }, _AnalyticsController_getEventFragment = function _AnalyticsController_getEventFragment(id) {
937
+ return this.state.eventFragments?.[id];
938
+ }, _AnalyticsController_setEventFragment = function _AnalyticsController_setEventFragment(fragment) {
939
+ const eventFragments = {
940
+ ...this.state.eventFragments,
941
+ [fragment.id]: fragment,
942
+ };
943
+ this.update((state) => {
944
+ state.eventFragments = eventFragments;
945
+ });
946
+ }, _AnalyticsController_removeEventFragment = function _AnalyticsController_removeEventFragment(id) {
947
+ const currentEventFragments = this.state.eventFragments;
948
+ if (!currentEventFragments ||
949
+ !Object.prototype.hasOwnProperty.call(currentEventFragments, id)) {
950
+ return;
951
+ }
952
+ const { [id]: _deletedFragment, ...eventFragments } = currentEventFragments;
953
+ this.update((state) => {
954
+ state.eventFragments = eventFragments;
955
+ });
956
+ }, _AnalyticsController_clearEventFragments = function _AnalyticsController_clearEventFragments() {
957
+ if (!this.state.eventFragments ||
958
+ Object.keys(this.state.eventFragments).length === 0) {
959
+ return;
960
+ }
961
+ this.update((state) => {
962
+ state.eventFragments = {};
963
+ });
964
+ }, _AnalyticsController_shouldIgnoreEventFragmentCall = function _AnalyticsController_shouldIgnoreEventFragmentCall(method) {
965
+ if (!__classPrivateFieldGet(this, _AnalyticsController_isEventFragmentsEnabled, "f")) {
966
+ (0, AnalyticsLogger_js_1.projectLogger)('Ignoring event fragment call because the event fragments feature is disabled', { method });
967
+ return true;
968
+ }
969
+ if (!__classPrivateFieldGet(this, _AnalyticsController_instances, "m", _AnalyticsController_isAnalyticsCaptureAllowed).call(this)) {
970
+ (0, AnalyticsLogger_js_1.projectLogger)('Ignoring event fragment call because the consent state does not allow capturing analytics', { method });
971
+ return true;
972
+ }
973
+ return false;
974
+ }, _AnalyticsController_emitEventFragment = function _AnalyticsController_emitEventFragment(fragment, name, context) {
975
+ const properties = { ...fragment.properties };
976
+ const sensitiveProperties = { ...fragment.sensitiveProperties };
977
+ this.trackEvent({
978
+ name,
979
+ properties,
980
+ sensitiveProperties,
981
+ saveDataRecording: false,
982
+ hasProperties: Object.keys(properties).length > 0 ||
983
+ Object.keys(sensitiveProperties).length > 0,
984
+ }, context);
985
+ }, _AnalyticsController_isAnalyticsCaptureAllowed = function _AnalyticsController_isAnalyticsCaptureAllowed() {
986
+ if (selectors_js_1.analyticsControllerSelectors.selectEnabled(this.state)) {
987
+ return true;
988
+ }
989
+ return __classPrivateFieldGet(this, _AnalyticsController_isPreConsentQueueEnabled, "f") && !this.state.consentDecisionMade;
640
990
  };
641
991
  //# sourceMappingURL=AnalyticsController.cjs.map