@metamask-previews/analytics-controller 3.0.0-preview-3e86f66f7 → 3.0.0-preview-9d2a9bdf5
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.
- package/CHANGELOG.md +1 -8
- package/README.md +8 -36
- package/dist/AnalyticsController-method-action-types.d.ts +1 -31
- package/dist/AnalyticsController-method-action-types.d.ts.map +1 -1
- package/dist/AnalyticsController-method-action-types.js.map +1 -1
- package/dist/AnalyticsController.d.ts +4 -77
- package/dist/AnalyticsController.d.ts.map +1 -1
- package/dist/AnalyticsController.js +171 -475
- package/dist/AnalyticsController.js.map +1 -1
- package/dist/AnalyticsPlatformAdapter.types.d.ts +2 -16
- package/dist/AnalyticsPlatformAdapter.types.d.ts.map +1 -1
- package/dist/AnalyticsPlatformAdapter.types.js.map +1 -1
- package/dist/EventFragment.types.d.ts +0 -11
- package/dist/EventFragment.types.d.ts.map +1 -1
- package/dist/EventFragment.types.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/selectors.d.ts +0 -16
- package/dist/selectors.d.ts.map +1 -1
- package/dist/selectors.js +0 -16
- package/dist/selectors.js.map +1 -1
- package/package.json +2 -3
|
@@ -21,20 +21,6 @@ export const controllerName = 'AnalyticsController';
|
|
|
21
21
|
* `properties` or `sensitiveProperties` in storage indefinitely.
|
|
22
22
|
*/
|
|
23
23
|
export const EVENT_FRAGMENT_MAX_AGE = 24 * 60 * 60 * 1000;
|
|
24
|
-
/**
|
|
25
|
-
* Purposes for which an analytics payload can be used.
|
|
26
|
-
*/
|
|
27
|
-
export const AnalyticsPurpose = {
|
|
28
|
-
Product: 'product',
|
|
29
|
-
Marketing: 'marketing',
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Persisted queues on {@link AnalyticsControllerState}.
|
|
33
|
-
*/
|
|
34
|
-
const AnalyticsQueue = {
|
|
35
|
-
EventQueue: 'eventQueue',
|
|
36
|
-
PreConsentEventQueue: 'preConsentEventQueue',
|
|
37
|
-
};
|
|
38
24
|
/**
|
|
39
25
|
* Returns default values for AnalyticsController state.
|
|
40
26
|
*
|
|
@@ -47,8 +33,6 @@ export function getDefaultAnalyticsControllerState() {
|
|
|
47
33
|
return {
|
|
48
34
|
optedIn: false,
|
|
49
35
|
consentDecisionMade: false,
|
|
50
|
-
optedInToMarketing: false,
|
|
51
|
-
marketingConsentDecisionMade: false,
|
|
52
36
|
};
|
|
53
37
|
}
|
|
54
38
|
/**
|
|
@@ -64,24 +48,6 @@ const analyticsControllerMetadata = {
|
|
|
64
48
|
includeInDebugSnapshot: true,
|
|
65
49
|
usedInUi: true,
|
|
66
50
|
},
|
|
67
|
-
optedInToMarketing: {
|
|
68
|
-
includeInStateLogs: true,
|
|
69
|
-
persist: true,
|
|
70
|
-
includeInDebugSnapshot: true,
|
|
71
|
-
usedInUi: true,
|
|
72
|
-
},
|
|
73
|
-
marketingConsentDecisionMade: {
|
|
74
|
-
includeInStateLogs: true,
|
|
75
|
-
persist: true,
|
|
76
|
-
includeInDebugSnapshot: true,
|
|
77
|
-
usedInUi: true,
|
|
78
|
-
},
|
|
79
|
-
eventsConfig: {
|
|
80
|
-
includeInStateLogs: true,
|
|
81
|
-
persist: true,
|
|
82
|
-
includeInDebugSnapshot: true,
|
|
83
|
-
usedInUi: false,
|
|
84
|
-
},
|
|
85
51
|
analyticsId: {
|
|
86
52
|
includeInStateLogs: true,
|
|
87
53
|
persist: true,
|
|
@@ -121,9 +87,6 @@ const MESSENGER_EXPOSED_METHODS = [
|
|
|
121
87
|
'optIn',
|
|
122
88
|
'optOut',
|
|
123
89
|
'resetConsentDecision',
|
|
124
|
-
'optInToMarketing',
|
|
125
|
-
'optOutOfMarketing',
|
|
126
|
-
'resetMarketingConsentDecision',
|
|
127
90
|
'createEventFragment',
|
|
128
91
|
'upsertEventFragment',
|
|
129
92
|
'updateEventFragment',
|
|
@@ -140,22 +103,6 @@ const MESSENGER_EXPOSED_METHODS = [
|
|
|
140
103
|
function isRecord(value) {
|
|
141
104
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
142
105
|
}
|
|
143
|
-
function isAnalyticsPurpose(value) {
|
|
144
|
-
return (value === AnalyticsPurpose.Product || value === AnalyticsPurpose.Marketing);
|
|
145
|
-
}
|
|
146
|
-
function isEventPurposesRecord(value) {
|
|
147
|
-
return (isRecord(value) &&
|
|
148
|
-
Object.values(value).every((purposes) => Array.isArray(purposes) &&
|
|
149
|
-
purposes.length > 0 &&
|
|
150
|
-
purposes.every(isAnalyticsPurpose)));
|
|
151
|
-
}
|
|
152
|
-
function isAnalyticsEventsConfig(value) {
|
|
153
|
-
return (isRecord(value) &&
|
|
154
|
-
typeof value.schemaVersion === 'string' &&
|
|
155
|
-
typeof value.version === 'string' &&
|
|
156
|
-
typeof value.timestamp === 'number' &&
|
|
157
|
-
isEventPurposesRecord(value.events));
|
|
158
|
-
}
|
|
159
106
|
/**
|
|
160
107
|
* Returns whether a JSON value is a non-array object.
|
|
161
108
|
*
|
|
@@ -197,12 +144,7 @@ function isAnalyticsQueuedEvent(value) {
|
|
|
197
144
|
return false;
|
|
198
145
|
}
|
|
199
146
|
if (typeof value.messageId !== 'string' ||
|
|
200
|
-
typeof value.timestamp !== 'string'
|
|
201
|
-
(value.eventPurposes !== undefined &&
|
|
202
|
-
(!Array.isArray(value.eventPurposes) ||
|
|
203
|
-
!value.eventPurposes.every(isAnalyticsPurpose))) ||
|
|
204
|
-
(value.eventsConfigVersion !== undefined &&
|
|
205
|
-
typeof value.eventsConfigVersion !== 'string')) {
|
|
147
|
+
typeof value.timestamp !== 'string') {
|
|
206
148
|
return false;
|
|
207
149
|
}
|
|
208
150
|
if (value.type === 'track') {
|
|
@@ -243,10 +185,6 @@ function isAnalyticsEventFragment(value) {
|
|
|
243
185
|
typeof value.successEvent === 'string') &&
|
|
244
186
|
(value.failureEvent === undefined ||
|
|
245
187
|
typeof value.failureEvent === 'string') &&
|
|
246
|
-
(value.eventPurposes === undefined ||
|
|
247
|
-
isEventPurposesRecord(value.eventPurposes)) &&
|
|
248
|
-
(value.eventsConfigVersion === undefined ||
|
|
249
|
-
typeof value.eventsConfigVersion === 'string') &&
|
|
250
188
|
(value.context === undefined || isRecord(value.context)) &&
|
|
251
189
|
(value.persist === undefined || typeof value.persist === 'boolean'));
|
|
252
190
|
}
|
|
@@ -276,17 +214,18 @@ function mergeEventFragment(fragment, payload) {
|
|
|
276
214
|
};
|
|
277
215
|
}
|
|
278
216
|
/**
|
|
279
|
-
* Merges two optional analytics contexts
|
|
217
|
+
* Merges two optional analytics contexts, preserving `undefined` when neither
|
|
218
|
+
* side has one so an empty context is never sent.
|
|
280
219
|
*
|
|
281
220
|
* @param base - The context to merge into.
|
|
282
|
-
* @param override - The context whose fields win.
|
|
221
|
+
* @param override - The context whose fields win.
|
|
283
222
|
* @returns The merged context, or `undefined` when both sides are unset.
|
|
284
223
|
*/
|
|
285
224
|
function mergeEventFragmentContext(base, override) {
|
|
286
|
-
if (override === undefined) {
|
|
287
|
-
return
|
|
225
|
+
if (base === undefined && override === undefined) {
|
|
226
|
+
return undefined;
|
|
288
227
|
}
|
|
289
|
-
return { ...(base ?? {}), ...override };
|
|
228
|
+
return { ...(base ?? {}), ...(override ?? {}) };
|
|
290
229
|
}
|
|
291
230
|
/**
|
|
292
231
|
* The AnalyticsController manages analytics tracking across platforms (Mobile/Extension).
|
|
@@ -308,11 +247,6 @@ export class AnalyticsController extends BaseController {
|
|
|
308
247
|
#isPreConsentQueueEnabled;
|
|
309
248
|
#isGeolocationEnabled;
|
|
310
249
|
#isEventFragmentsEnabled;
|
|
311
|
-
/**
|
|
312
|
-
* In-memory event-purpose lookup from persisted state.
|
|
313
|
-
*/
|
|
314
|
-
#eventPurposes;
|
|
315
|
-
#eventsConfigVersion;
|
|
316
250
|
/**
|
|
317
251
|
* The in-flight (or settled) initialization promise. Set on the first
|
|
318
252
|
* {@link init} call and returned by subsequent calls so overlapping callers
|
|
@@ -346,15 +280,6 @@ export class AnalyticsController extends BaseController {
|
|
|
346
280
|
...getDefaultAnalyticsControllerState(),
|
|
347
281
|
...state,
|
|
348
282
|
};
|
|
349
|
-
const eventsConfig = isAnalyticsEventsConfig(initialState.eventsConfig)
|
|
350
|
-
? initialState.eventsConfig
|
|
351
|
-
: undefined;
|
|
352
|
-
if (eventsConfig === undefined) {
|
|
353
|
-
delete initialState.eventsConfig;
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
initialState.eventsConfig = eventsConfig;
|
|
357
|
-
}
|
|
358
283
|
validateAnalyticsControllerState(initialState, platformAdapter.skipUUIDv4Check === true);
|
|
359
284
|
super({
|
|
360
285
|
name: controllerName,
|
|
@@ -370,14 +295,10 @@ export class AnalyticsController extends BaseController {
|
|
|
370
295
|
this.#platformAdapter = platformAdapter;
|
|
371
296
|
this.#initPromise = undefined;
|
|
372
297
|
this.#locationResolvePromise = undefined;
|
|
373
|
-
this.#eventPurposes = new Map(Object.entries(eventsConfig?.events ?? {}));
|
|
374
|
-
this.#eventsConfigVersion = eventsConfig?.version;
|
|
375
298
|
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
376
299
|
log('AnalyticsController initialized and ready', {
|
|
377
300
|
enabled: analyticsControllerSelectors.selectEnabled(this.state),
|
|
378
301
|
optedIn: this.state.optedIn,
|
|
379
|
-
optedInToMarketing: this.state.optedInToMarketing === true,
|
|
380
|
-
marketingConsentDecisionMade: this.state.marketingConsentDecisionMade === true,
|
|
381
302
|
consentDecisionMade: this.state.consentDecisionMade,
|
|
382
303
|
analyticsId: this.state.analyticsId,
|
|
383
304
|
eventQueuePersistenceEnabled: this.#isEventQueuePersistenceEnabled,
|
|
@@ -392,11 +313,10 @@ export class AnalyticsController extends BaseController {
|
|
|
392
313
|
* method must be called after construction to complete the setup process.
|
|
393
314
|
*
|
|
394
315
|
* When geolocation enrichment is enabled (`isGeolocationEnabled`), geolocation
|
|
395
|
-
* is resolved only for a user who is already opted in
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* they consent to analytics. In either case the `GeolocationController` and its
|
|
316
|
+
* is resolved only for a user who is already opted in; for undecided or
|
|
317
|
+
* opted-out users it is deferred until they opt in (see {@link optIn}), so a
|
|
318
|
+
* user's location is never requested before they consent to analytics. In
|
|
319
|
+
* either case the `GeolocationController` and its
|
|
400
320
|
* `GeolocationController:getGeolocationData` action must be registered before
|
|
401
321
|
* resolution occurs, or enrichment is skipped for the session (a message is
|
|
402
322
|
* logged, see {@link #resolveLocationContext}).
|
|
@@ -430,11 +350,9 @@ export class AnalyticsController extends BaseController {
|
|
|
430
350
|
initEventFragmentSnapshot.set(id, fragment.createdAt);
|
|
431
351
|
}
|
|
432
352
|
}
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
// {@link optIn} / {@link optInToMarketing}. Awaited so that an already-opted-in
|
|
437
|
-
// session has location available before events replay.
|
|
353
|
+
// Resolve geolocation only when the user is already opted in; for undecided
|
|
354
|
+
// or opted-out users it is deferred to {@link optIn}. Awaited so that an
|
|
355
|
+
// already-opted-in session has location available before events replay.
|
|
438
356
|
await this.#maybeResolveLocation();
|
|
439
357
|
// Call onSetupCompleted lifecycle hook after initialization
|
|
440
358
|
// State is already validated, so analyticsId is guaranteed to be a valid UUIDv4
|
|
@@ -446,20 +364,20 @@ export class AnalyticsController extends BaseController {
|
|
|
446
364
|
log('Error calling platformAdapter.onSetupCompleted', error);
|
|
447
365
|
}
|
|
448
366
|
this.#replayQueuedEvents();
|
|
449
|
-
this.#
|
|
367
|
+
this.#reconcilePreConsentEvents();
|
|
450
368
|
this.#reconcileEventFragments(initEventFragmentSnapshot);
|
|
451
369
|
}
|
|
452
370
|
/**
|
|
453
371
|
* Start resolving the geolocation context if warranted, and return the
|
|
454
372
|
* in-flight (or settled) resolution so callers can await it. No-op unless
|
|
455
|
-
* enrichment is enabled, the user is opted in
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
373
|
+
* enrichment is enabled, the user is opted in, and a resolution has not
|
|
374
|
+
* already been started. Deferring resolution until opt-in ensures a user's
|
|
375
|
+
* location is never requested before they consent to analytics (for example,
|
|
376
|
+
* during onboarding).
|
|
459
377
|
*
|
|
460
378
|
* Resolution runs at most once per controller session: the settled promise
|
|
461
|
-
* is retained, so the outcome
|
|
462
|
-
* {@link #resolveLocationContext}) is not retried, and events are delivered
|
|
379
|
+
* is retained, so the outcome — including a failure (see
|
|
380
|
+
* {@link #resolveLocationContext}) — is not retried, and events are delivered
|
|
463
381
|
* without location for the rest of the session.
|
|
464
382
|
*
|
|
465
383
|
* @returns The geolocation resolution promise, or `undefined` when no
|
|
@@ -468,7 +386,7 @@ export class AnalyticsController extends BaseController {
|
|
|
468
386
|
#maybeResolveLocation() {
|
|
469
387
|
if (this.#isGeolocationEnabled &&
|
|
470
388
|
this.#locationResolvePromise === undefined &&
|
|
471
|
-
(this.state
|
|
389
|
+
analyticsControllerSelectors.selectEnabled(this.state)) {
|
|
472
390
|
this.#locationResolvePromise = this.#resolveLocationContext();
|
|
473
391
|
}
|
|
474
392
|
return this.#locationResolvePromise;
|
|
@@ -509,131 +427,18 @@ export class AnalyticsController extends BaseController {
|
|
|
509
427
|
},
|
|
510
428
|
};
|
|
511
429
|
}
|
|
512
|
-
/**
|
|
513
|
-
* Stamp the purposes currently allowed for a payload using Segment's consent
|
|
514
|
-
* context shape.
|
|
515
|
-
*
|
|
516
|
-
* @param purposes - Purposes for which the payload is eligible.
|
|
517
|
-
* @param context - Optional caller-provided context.
|
|
518
|
-
* @param version - Config version captured with the payload.
|
|
519
|
-
* @returns Context with allowed purpose preferences.
|
|
520
|
-
*/
|
|
521
|
-
#withConsentContext(purposes, context, version) {
|
|
522
|
-
const preferences = this.#allowedPurposePreferences(purposes);
|
|
523
|
-
const { consent: existingConsent, eventsConfigVersion: _ignoredVersion, ...unmanagedContext } = context ?? {};
|
|
524
|
-
const consent = isJsonRecord(existingConsent)
|
|
525
|
-
? existingConsent
|
|
526
|
-
: {};
|
|
527
|
-
const existingCategoryPreferences = isJsonRecord(consent.categoryPreferences)
|
|
528
|
-
? consent.categoryPreferences
|
|
529
|
-
: {};
|
|
530
|
-
return {
|
|
531
|
-
...unmanagedContext,
|
|
532
|
-
consent: {
|
|
533
|
-
...consent,
|
|
534
|
-
categoryPreferences: {
|
|
535
|
-
...existingCategoryPreferences,
|
|
536
|
-
...preferences,
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
...(version === undefined ? {} : { eventsConfigVersion: version }),
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Load event-purpose configuration.
|
|
544
|
-
*
|
|
545
|
-
* Phase 1 stub. Persisted configuration remains authoritative until a remote
|
|
546
|
-
* source is wired up.
|
|
547
|
-
*/
|
|
548
|
-
async #fetchEventsConfig() {
|
|
549
|
-
// Intentionally empty until an events-config source is wired up.
|
|
550
|
-
}
|
|
551
|
-
#purposesFromName(name) {
|
|
552
|
-
return [...(this.#eventPurposes.get(name) ?? [AnalyticsPurpose.Product])];
|
|
553
|
-
}
|
|
554
|
-
#purposesFromQueuedEvent(queuedEvent) {
|
|
555
|
-
if (queuedEvent.type === 'identify') {
|
|
556
|
-
return [AnalyticsPurpose.Product];
|
|
557
|
-
}
|
|
558
|
-
if (queuedEvent.eventPurposes !== undefined) {
|
|
559
|
-
return [...queuedEvent.eventPurposes];
|
|
560
|
-
}
|
|
561
|
-
return this.#purposesFromName(queuedEvent.type === 'view' ? queuedEvent.name : queuedEvent.eventName);
|
|
562
|
-
}
|
|
563
|
-
#eventNamesFromFragment(fragment) {
|
|
564
|
-
return [
|
|
565
|
-
fragment.initialEvent,
|
|
566
|
-
fragment.successEvent,
|
|
567
|
-
fragment.failureEvent,
|
|
568
|
-
].filter((name) => typeof name === 'string');
|
|
569
|
-
}
|
|
570
|
-
#purposesFromFragmentEvent(fragment, name) {
|
|
571
|
-
return [
|
|
572
|
-
...(fragment.eventPurposes?.[name] ?? this.#purposesFromName(name)),
|
|
573
|
-
];
|
|
574
|
-
}
|
|
575
|
-
#purposesFromFragment(fragment) {
|
|
576
|
-
const names = this.#eventNamesFromFragment(fragment);
|
|
577
|
-
if (names.length === 0) {
|
|
578
|
-
// Nameless property bags have no event to classify. Treat them as
|
|
579
|
-
// eligible for any purpose so they can accumulate when either consent
|
|
580
|
-
// allows capture.
|
|
581
|
-
return Object.values(AnalyticsPurpose);
|
|
582
|
-
}
|
|
583
|
-
return [
|
|
584
|
-
...new Set(names.flatMap((name) => this.#purposesFromFragmentEvent(fragment, name))),
|
|
585
|
-
];
|
|
586
|
-
}
|
|
587
|
-
#consent(purpose) {
|
|
588
|
-
return purpose === AnalyticsPurpose.Marketing
|
|
589
|
-
? {
|
|
590
|
-
optedIn: this.state.optedInToMarketing === true,
|
|
591
|
-
decisionMade: this.state.marketingConsentDecisionMade === true,
|
|
592
|
-
}
|
|
593
|
-
: {
|
|
594
|
-
optedIn: this.state.optedIn,
|
|
595
|
-
decisionMade: this.state.consentDecisionMade === true,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
#allowedPurposePreferences(purposes) {
|
|
599
|
-
return {
|
|
600
|
-
product: purposes.includes(AnalyticsPurpose.Product) &&
|
|
601
|
-
this.#consent(AnalyticsPurpose.Product).optedIn,
|
|
602
|
-
marketing: purposes.includes(AnalyticsPurpose.Marketing) &&
|
|
603
|
-
this.#consent(AnalyticsPurpose.Marketing).optedIn,
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
#hasAllowedPurpose(purposes) {
|
|
607
|
-
const { product, marketing } = this.#allowedPurposePreferences(purposes);
|
|
608
|
-
return product || marketing;
|
|
609
|
-
}
|
|
610
|
-
#hasUndecidedPurpose(purposes) {
|
|
611
|
-
return purposes.some((purpose) => !this.#consent(purpose).decisionMade);
|
|
612
|
-
}
|
|
613
|
-
#isCaptureAllowed(purposes) {
|
|
614
|
-
return (this.#hasAllowedPurpose(purposes) ||
|
|
615
|
-
(this.#isPreConsentQueueEnabled && this.#hasUndecidedPurpose(purposes)));
|
|
616
|
-
}
|
|
617
|
-
#replaceQueue(field, nextQueue) {
|
|
618
|
-
this.update((state) => {
|
|
619
|
-
state[field] = nextQueue;
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
430
|
/**
|
|
623
431
|
* Send final track payload through the platform adapter or queue it if persistence is enabled.
|
|
624
432
|
*
|
|
625
433
|
* @param eventName - The name of the event.
|
|
626
434
|
* @param properties - Optional event properties.
|
|
627
435
|
* @param context - Optional platform-specific context.
|
|
628
|
-
* @param purposes - Capture-time purposes for the event.
|
|
629
|
-
* @param version - Capture-time events config version.
|
|
630
436
|
*/
|
|
631
|
-
#sendOrQueueTrackEvent(eventName, properties, context
|
|
632
|
-
const isAllowed = this.#hasAllowedPurpose(purposes);
|
|
633
|
-
const contextWithConsent = this.#withConsentContext(purposes, context, version);
|
|
437
|
+
#sendOrQueueTrackEvent(eventName, properties, context) {
|
|
634
438
|
// Direct delivery: enabled and not persisting.
|
|
635
|
-
if (
|
|
636
|
-
this.#
|
|
439
|
+
if (analyticsControllerSelectors.selectEnabled(this.state) &&
|
|
440
|
+
!this.#isEventQueuePersistenceEnabled) {
|
|
441
|
+
this.#platformAdapter.track(eventName, properties, context);
|
|
637
442
|
return;
|
|
638
443
|
}
|
|
639
444
|
const queuedEvent = {
|
|
@@ -642,11 +447,11 @@ export class AnalyticsController extends BaseController {
|
|
|
642
447
|
messageId: uuid(),
|
|
643
448
|
timestamp: new Date().toISOString(),
|
|
644
449
|
...(properties === undefined ? {} : { properties }),
|
|
645
|
-
context:
|
|
646
|
-
eventPurposes: purposes,
|
|
647
|
-
...(version === undefined ? {} : { eventsConfigVersion: version }),
|
|
450
|
+
...(context === undefined ? {} : { context }),
|
|
648
451
|
};
|
|
649
|
-
|
|
452
|
+
// Not yet enabled (reached only while undecided with the pre-consent queue
|
|
453
|
+
// enabled): hold the event until the user opts in.
|
|
454
|
+
if (!analyticsControllerSelectors.selectEnabled(this.state)) {
|
|
650
455
|
this.#enqueuePreConsentEvent(queuedEvent);
|
|
651
456
|
return;
|
|
652
457
|
}
|
|
@@ -660,10 +465,8 @@ export class AnalyticsController extends BaseController {
|
|
|
660
465
|
* @param context - Optional platform-specific context.
|
|
661
466
|
*/
|
|
662
467
|
#sendOrQueueIdentifyEvent(userId, traits, context) {
|
|
663
|
-
const purposes = [AnalyticsPurpose.Product];
|
|
664
|
-
const contextWithConsent = this.#withConsentContext(purposes, context);
|
|
665
468
|
if (!this.#isEventQueuePersistenceEnabled) {
|
|
666
|
-
this.#platformAdapter.identify(userId, traits,
|
|
469
|
+
this.#platformAdapter.identify(userId, traits, context);
|
|
667
470
|
return;
|
|
668
471
|
}
|
|
669
472
|
const queuedEvent = {
|
|
@@ -672,8 +475,7 @@ export class AnalyticsController extends BaseController {
|
|
|
672
475
|
messageId: uuid(),
|
|
673
476
|
timestamp: new Date().toISOString(),
|
|
674
477
|
...(traits === undefined ? {} : { traits }),
|
|
675
|
-
context:
|
|
676
|
-
eventPurposes: purposes,
|
|
478
|
+
...(context === undefined ? {} : { context }),
|
|
677
479
|
};
|
|
678
480
|
this.#enqueueEvent(queuedEvent);
|
|
679
481
|
}
|
|
@@ -683,14 +485,10 @@ export class AnalyticsController extends BaseController {
|
|
|
683
485
|
* @param name - The view name.
|
|
684
486
|
* @param properties - Optional view properties.
|
|
685
487
|
* @param context - Optional platform-specific context.
|
|
686
|
-
* @param purposes - Capture-time purposes for the view.
|
|
687
|
-
* @param version - Capture-time events config version.
|
|
688
488
|
*/
|
|
689
|
-
#sendOrQueueViewEvent(name, properties, context
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (isAllowed && !this.#isEventQueuePersistenceEnabled) {
|
|
693
|
-
this.#platformAdapter.view(name, properties, contextWithConsent);
|
|
489
|
+
#sendOrQueueViewEvent(name, properties, context) {
|
|
490
|
+
if (!this.#isEventQueuePersistenceEnabled) {
|
|
491
|
+
this.#platformAdapter.view(name, properties, context);
|
|
694
492
|
return;
|
|
695
493
|
}
|
|
696
494
|
const queuedEvent = {
|
|
@@ -699,14 +497,8 @@ export class AnalyticsController extends BaseController {
|
|
|
699
497
|
messageId: uuid(),
|
|
700
498
|
timestamp: new Date().toISOString(),
|
|
701
499
|
...(properties === undefined ? {} : { properties }),
|
|
702
|
-
context:
|
|
703
|
-
eventPurposes: purposes,
|
|
704
|
-
...(version === undefined ? {} : { eventsConfigVersion: version }),
|
|
500
|
+
...(context === undefined ? {} : { context }),
|
|
705
501
|
};
|
|
706
|
-
if (!isAllowed) {
|
|
707
|
-
this.#enqueuePreConsentEvent(queuedEvent);
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
502
|
this.#enqueueEvent(queuedEvent);
|
|
711
503
|
}
|
|
712
504
|
/**
|
|
@@ -776,23 +568,17 @@ export class AnalyticsController extends BaseController {
|
|
|
776
568
|
if (!this.#isEventQueuePersistenceEnabled || !this.state.eventQueue) {
|
|
777
569
|
return;
|
|
778
570
|
}
|
|
779
|
-
|
|
780
|
-
|
|
571
|
+
if (!analyticsControllerSelectors.selectEnabled(this.state)) {
|
|
572
|
+
this.#clearQueuedEvents();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
781
575
|
for (const [messageId, queuedEvent] of Object.entries(this.state.eventQueue)) {
|
|
782
576
|
if (!isAnalyticsQueuedEvent(queuedEvent) ||
|
|
783
577
|
queuedEvent.messageId !== messageId) {
|
|
784
578
|
log('Dropping invalid queued analytics event', { messageId });
|
|
579
|
+
this.#removeQueuedEvent(messageId);
|
|
785
580
|
continue;
|
|
786
581
|
}
|
|
787
|
-
const purposes = this.#purposesFromQueuedEvent(queuedEvent);
|
|
788
|
-
if (this.#hasAllowedPurpose(purposes)) {
|
|
789
|
-
const refreshedEvent = this.#refreshQueuedEventConsent(queuedEvent);
|
|
790
|
-
remainingQueue[messageId] = refreshedEvent;
|
|
791
|
-
eventsToSend.push(refreshedEvent);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
this.#replaceQueue(AnalyticsQueue.EventQueue, remainingQueue);
|
|
795
|
-
for (const queuedEvent of eventsToSend) {
|
|
796
582
|
this.#sendQueuedEvent(queuedEvent);
|
|
797
583
|
}
|
|
798
584
|
}
|
|
@@ -812,59 +598,17 @@ export class AnalyticsController extends BaseController {
|
|
|
812
598
|
state.eventQueue = eventQueue;
|
|
813
599
|
});
|
|
814
600
|
}
|
|
815
|
-
#refreshQueuedEventConsent(queuedEvent) {
|
|
816
|
-
// Refresh only the consent stamp. Capture-time `eventPurposes` and
|
|
817
|
-
// `eventsConfigVersion` stay as a pair and are never rewritten here.
|
|
818
|
-
const purposes = this.#purposesFromQueuedEvent(queuedEvent);
|
|
819
|
-
const preferences = this.#allowedPurposePreferences(purposes);
|
|
820
|
-
const existingPreferences = isJsonRecord(queuedEvent.context?.consent)
|
|
821
|
-
? queuedEvent.context.consent.categoryPreferences
|
|
822
|
-
: undefined;
|
|
823
|
-
if (isJsonRecord(existingPreferences) &&
|
|
824
|
-
existingPreferences.product === preferences.product &&
|
|
825
|
-
existingPreferences.marketing === preferences.marketing) {
|
|
826
|
-
return queuedEvent;
|
|
827
|
-
}
|
|
828
|
-
return {
|
|
829
|
-
...queuedEvent,
|
|
830
|
-
context: this.#withConsentContext(purposes, queuedEvent.context, queuedEvent.eventsConfigVersion),
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
601
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* For {@link AnalyticsQueue.EventQueue}, entries are kept only while at least
|
|
837
|
-
* one capture-time purpose is opted in, and their consent stamp is refreshed.
|
|
838
|
-
* For {@link AnalyticsQueue.PreConsentEventQueue}, entries are kept while at
|
|
839
|
-
* least one purpose is still allowed or undecided.
|
|
840
|
-
*
|
|
841
|
-
* @param field - The queue to prune.
|
|
602
|
+
* Clear all queued analytics events.
|
|
842
603
|
*/
|
|
843
|
-
#
|
|
844
|
-
|
|
845
|
-
|
|
604
|
+
#clearQueuedEvents() {
|
|
605
|
+
if (!this.state.eventQueue ||
|
|
606
|
+
Object.keys(this.state.eventQueue).length === 0) {
|
|
846
607
|
return;
|
|
847
608
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
queuedEvent.messageId !== messageId) {
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
const purposes = this.#purposesFromQueuedEvent(queuedEvent);
|
|
855
|
-
const isAllowed = this.#hasAllowedPurpose(purposes);
|
|
856
|
-
if (field === AnalyticsQueue.EventQueue) {
|
|
857
|
-
if (isAllowed) {
|
|
858
|
-
nextQueue[messageId] = this.#refreshQueuedEventConsent(queuedEvent);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
if (field === AnalyticsQueue.PreConsentEventQueue) {
|
|
862
|
-
if (isAllowed || this.#hasUndecidedPurpose(purposes)) {
|
|
863
|
-
nextQueue[messageId] = queuedEvent;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
this.#replaceQueue(field, nextQueue);
|
|
609
|
+
this.update((state) => {
|
|
610
|
+
state.eventQueue = {};
|
|
611
|
+
});
|
|
868
612
|
}
|
|
869
613
|
/**
|
|
870
614
|
* Add an event to the pre-consent queue without delivering it.
|
|
@@ -880,6 +624,34 @@ export class AnalyticsController extends BaseController {
|
|
|
880
624
|
state.preConsentEventQueue = preConsentEventQueue;
|
|
881
625
|
});
|
|
882
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Replay queued pre-consent events through the delivery path.
|
|
629
|
+
*
|
|
630
|
+
* Only called by {@link #reconcilePreConsentEvents}, which guarantees the
|
|
631
|
+
* pre-consent queue is enabled and that the user is opted in. The queue is
|
|
632
|
+
* cleared before replaying so events cannot be re-queued or replayed twice.
|
|
633
|
+
*
|
|
634
|
+
* @param queue - The pre-consent event queue to replay.
|
|
635
|
+
*/
|
|
636
|
+
#replayPreConsentEvents(queue) {
|
|
637
|
+
this.#clearPreConsentEvents();
|
|
638
|
+
for (const [messageId, queuedEvent] of Object.entries(queue)) {
|
|
639
|
+
if (!isAnalyticsQueuedEvent(queuedEvent) ||
|
|
640
|
+
queuedEvent.messageId !== messageId) {
|
|
641
|
+
log('Dropping invalid queued pre-consent analytics event', {
|
|
642
|
+
messageId,
|
|
643
|
+
});
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const eventToReplay = this.#enrichPreConsentEvent(queuedEvent);
|
|
647
|
+
if (this.#isEventQueuePersistenceEnabled) {
|
|
648
|
+
this.#enqueueEvent(eventToReplay);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
this.#sendQueuedEvent(eventToReplay);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
883
655
|
/**
|
|
884
656
|
* Enrich a pre-consent event with the geolocation resolved on opt-in.
|
|
885
657
|
*
|
|
@@ -903,52 +675,42 @@ export class AnalyticsController extends BaseController {
|
|
|
903
675
|
};
|
|
904
676
|
}
|
|
905
677
|
/**
|
|
906
|
-
*
|
|
678
|
+
* Clear all queued pre-consent events.
|
|
679
|
+
*/
|
|
680
|
+
#clearPreConsentEvents() {
|
|
681
|
+
if (!this.state.preConsentEventQueue) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.update((state) => {
|
|
685
|
+
state.preConsentEventQueue = {};
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Reconcile the pre-consent queue on initialization.
|
|
907
690
|
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
*
|
|
691
|
+
* The queue should normally be empty unless the user is still undecided. This
|
|
692
|
+
* handles the rare cases where a consent decision was persisted but the queue
|
|
693
|
+
* was not flushed/cleared (e.g. an interrupted shutdown): replay it if the
|
|
694
|
+
* user is opted in, or clear it if they opted out.
|
|
912
695
|
*
|
|
913
696
|
* If the pre-consent queue is disabled, any stale persisted entries (e.g. from
|
|
914
697
|
* a previous session where it was enabled) are dropped so they can never be
|
|
915
698
|
* replayed.
|
|
916
699
|
*/
|
|
917
|
-
#
|
|
700
|
+
#reconcilePreConsentEvents() {
|
|
918
701
|
const queue = this.state.preConsentEventQueue;
|
|
919
702
|
if (!queue) {
|
|
920
703
|
return;
|
|
921
704
|
}
|
|
922
705
|
if (!this.#isPreConsentQueueEnabled) {
|
|
923
|
-
this
|
|
924
|
-
state.preConsentEventQueue = {};
|
|
925
|
-
});
|
|
706
|
+
this.#clearPreConsentEvents();
|
|
926
707
|
return;
|
|
927
708
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
for (const [messageId, queuedEvent] of Object.entries(queue)) {
|
|
931
|
-
if (!isAnalyticsQueuedEvent(queuedEvent) ||
|
|
932
|
-
queuedEvent.messageId !== messageId) {
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
const purposes = this.#purposesFromQueuedEvent(queuedEvent);
|
|
936
|
-
if (this.#hasAllowedPurpose(purposes)) {
|
|
937
|
-
replay.push(queuedEvent);
|
|
938
|
-
}
|
|
939
|
-
else if (this.#hasUndecidedPurpose(purposes)) {
|
|
940
|
-
keep[messageId] = queuedEvent;
|
|
941
|
-
}
|
|
709
|
+
if (this.state.optedIn) {
|
|
710
|
+
this.#replayPreConsentEvents(queue);
|
|
942
711
|
}
|
|
943
|
-
this
|
|
944
|
-
|
|
945
|
-
const eventToReplay = this.#refreshQueuedEventConsent(this.#enrichPreConsentEvent(queuedEvent));
|
|
946
|
-
if (this.#isEventQueuePersistenceEnabled) {
|
|
947
|
-
this.#enqueueEvent(eventToReplay);
|
|
948
|
-
}
|
|
949
|
-
else {
|
|
950
|
-
this.#sendQueuedEvent(eventToReplay);
|
|
951
|
-
}
|
|
712
|
+
else if (this.state.consentDecisionMade) {
|
|
713
|
+
this.#clearPreConsentEvents();
|
|
952
714
|
}
|
|
953
715
|
}
|
|
954
716
|
/**
|
|
@@ -961,8 +723,9 @@ export class AnalyticsController extends BaseController {
|
|
|
961
723
|
* finalization is not a failure, just an unfinished one.
|
|
962
724
|
*
|
|
963
725
|
* If the feature is disabled (e.g. a previous session had it enabled), or the
|
|
964
|
-
* consent state no longer allows capture
|
|
965
|
-
*
|
|
726
|
+
* consent state no longer allows capture (e.g. the fragments were written
|
|
727
|
+
* before the user opted out), every persisted fragment is dropped so none of
|
|
728
|
+
* them can linger.
|
|
966
729
|
*
|
|
967
730
|
* Non-persistent fragments are dropped only when their ID and `createdAt`
|
|
968
731
|
* match a fragment present at the start of {@link init}. Fragments created
|
|
@@ -977,13 +740,28 @@ export class AnalyticsController extends BaseController {
|
|
|
977
740
|
if (!fragments) {
|
|
978
741
|
return;
|
|
979
742
|
}
|
|
980
|
-
if (!this.#isEventFragmentsEnabled) {
|
|
743
|
+
if (!this.#isEventFragmentsEnabled || !this.#isAnalyticsCaptureAllowed()) {
|
|
981
744
|
this.#clearEventFragments();
|
|
982
745
|
return;
|
|
983
746
|
}
|
|
747
|
+
this.#purgeStaleEventFragments(fragments, initEventFragmentSnapshot);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Drop every persisted fragment that is invalid, expired, did not opt into
|
|
751
|
+
* `persist`, or was already present with the same `createdAt` when
|
|
752
|
+
* {@link init} began.
|
|
753
|
+
*
|
|
754
|
+
* Only called by {@link #reconcileEventFragments}, which guarantees the
|
|
755
|
+
* fragments exist and that the event fragments feature is enabled.
|
|
756
|
+
*
|
|
757
|
+
* @param currentEventFragments - The persisted fragments to filter.
|
|
758
|
+
* @param initEventFragmentSnapshot - Fragment IDs and `createdAt` values
|
|
759
|
+
* present when {@link init} began.
|
|
760
|
+
*/
|
|
761
|
+
#purgeStaleEventFragments(currentEventFragments, initEventFragmentSnapshot) {
|
|
984
762
|
const eventFragments = {};
|
|
985
763
|
const now = Date.now();
|
|
986
|
-
for (const [id, fragment] of Object.entries(
|
|
764
|
+
for (const [id, fragment] of Object.entries(currentEventFragments)) {
|
|
987
765
|
if (!isAnalyticsEventFragment(fragment) || fragment.id !== id) {
|
|
988
766
|
log('Dropping invalid persisted event fragment', { id });
|
|
989
767
|
continue;
|
|
@@ -992,9 +770,6 @@ export class AnalyticsController extends BaseController {
|
|
|
992
770
|
log('Dropping expired persisted event fragment', { id });
|
|
993
771
|
continue;
|
|
994
772
|
}
|
|
995
|
-
if (!this.#isCaptureAllowed(this.#purposesFromFragment(fragment))) {
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
773
|
const snapshotCreatedAt = initEventFragmentSnapshot.get(id);
|
|
999
774
|
if (fragment.persist === true ||
|
|
1000
775
|
snapshotCreatedAt === undefined ||
|
|
@@ -1002,11 +777,8 @@ export class AnalyticsController extends BaseController {
|
|
|
1002
777
|
eventFragments[id] = fragment;
|
|
1003
778
|
}
|
|
1004
779
|
}
|
|
1005
|
-
if (Object.keys(eventFragments).length ===
|
|
1006
|
-
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
if (Object.keys(eventFragments).length === Object.keys(fragments).length) {
|
|
780
|
+
if (Object.keys(eventFragments).length ===
|
|
781
|
+
Object.keys(currentEventFragments).length) {
|
|
1010
782
|
return;
|
|
1011
783
|
}
|
|
1012
784
|
this.update((state) => {
|
|
@@ -1026,31 +798,15 @@ export class AnalyticsController extends BaseController {
|
|
|
1026
798
|
* Write an event fragment to state, replacing any fragment with the same ID.
|
|
1027
799
|
*
|
|
1028
800
|
* @param fragment - The fragment to store.
|
|
1029
|
-
* @returns The stored fragment with capture-time purpose metadata.
|
|
1030
801
|
*/
|
|
1031
802
|
#setEventFragment(fragment) {
|
|
1032
|
-
// Snapshot classification only. Consent is stamped at emit time by
|
|
1033
|
-
// {@link #trackEvent}, so fragment.context stays caller metadata.
|
|
1034
|
-
let fragmentWithPurposeSnapshot = fragment;
|
|
1035
|
-
if (fragment.eventPurposes === undefined) {
|
|
1036
|
-
const names = this.#eventNamesFromFragment(fragment);
|
|
1037
|
-
const eventPurposes = Object.fromEntries(names.map((name) => [name, this.#purposesFromName(name)]));
|
|
1038
|
-
fragmentWithPurposeSnapshot = {
|
|
1039
|
-
...fragment,
|
|
1040
|
-
...(names.length === 0 ? {} : { eventPurposes }),
|
|
1041
|
-
...(this.#eventsConfigVersion === undefined
|
|
1042
|
-
? {}
|
|
1043
|
-
: { eventsConfigVersion: this.#eventsConfigVersion }),
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
803
|
const eventFragments = {
|
|
1047
804
|
...this.state.eventFragments,
|
|
1048
|
-
[
|
|
805
|
+
[fragment.id]: fragment,
|
|
1049
806
|
};
|
|
1050
807
|
this.update((state) => {
|
|
1051
808
|
state.eventFragments = eventFragments;
|
|
1052
809
|
});
|
|
1053
|
-
return fragmentWithPurposeSnapshot;
|
|
1054
810
|
}
|
|
1055
811
|
/**
|
|
1056
812
|
* Remove an event fragment from state.
|
|
@@ -1068,35 +824,6 @@ export class AnalyticsController extends BaseController {
|
|
|
1068
824
|
state.eventFragments = eventFragments;
|
|
1069
825
|
});
|
|
1070
826
|
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Drop event fragments that the current consent state no longer allows to
|
|
1073
|
-
* accumulate.
|
|
1074
|
-
*/
|
|
1075
|
-
#pruneEventFragmentsForConsent() {
|
|
1076
|
-
const fragments = this.state.eventFragments;
|
|
1077
|
-
if (!fragments || Object.keys(fragments).length === 0) {
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
const eventFragments = {};
|
|
1081
|
-
for (const [id, fragment] of Object.entries(fragments)) {
|
|
1082
|
-
if (isAnalyticsEventFragment(fragment) &&
|
|
1083
|
-
this.#isCaptureAllowed(this.#purposesFromFragment(fragment))) {
|
|
1084
|
-
eventFragments[id] = fragment;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
this.update((state) => {
|
|
1088
|
-
state.eventFragments = eventFragments;
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Drop queued events and fragments that the current consent state no longer
|
|
1093
|
-
* allows to keep.
|
|
1094
|
-
*/
|
|
1095
|
-
#pruneAllForConsent() {
|
|
1096
|
-
this.#pruneQueueForConsent(AnalyticsQueue.EventQueue);
|
|
1097
|
-
this.#pruneQueueForConsent(AnalyticsQueue.PreConsentEventQueue);
|
|
1098
|
-
this.#pruneEventFragmentsForConsent();
|
|
1099
|
-
}
|
|
1100
827
|
/**
|
|
1101
828
|
* Clear all event fragments.
|
|
1102
829
|
*/
|
|
@@ -1119,18 +846,14 @@ export class AnalyticsController extends BaseController {
|
|
|
1119
846
|
* fragment never accumulates data for an event that could not be delivered.
|
|
1120
847
|
*
|
|
1121
848
|
* @param method - The name of the method that was called.
|
|
1122
|
-
* @param fragment - The fragment being read or written, when one is known.
|
|
1123
849
|
* @returns True when the call should be ignored.
|
|
1124
850
|
*/
|
|
1125
|
-
#shouldIgnoreEventFragmentCall(method
|
|
851
|
+
#shouldIgnoreEventFragmentCall(method) {
|
|
1126
852
|
if (!this.#isEventFragmentsEnabled) {
|
|
1127
853
|
log('Ignoring event fragment call because the event fragments feature is disabled', { method });
|
|
1128
854
|
return true;
|
|
1129
855
|
}
|
|
1130
|
-
|
|
1131
|
-
? this.#isCaptureAllowed(this.#purposesFromFragment(fragment))
|
|
1132
|
-
: this.#isCaptureAllowed(Object.values(AnalyticsPurpose));
|
|
1133
|
-
if (!captureAllowed) {
|
|
856
|
+
if (!this.#isAnalyticsCaptureAllowed()) {
|
|
1134
857
|
log('Ignoring event fragment call because the consent state does not allow capturing analytics', { method });
|
|
1135
858
|
return true;
|
|
1136
859
|
}
|
|
@@ -1150,14 +873,32 @@ export class AnalyticsController extends BaseController {
|
|
|
1150
873
|
#emitEventFragment(fragment, name, context) {
|
|
1151
874
|
const properties = { ...fragment.properties };
|
|
1152
875
|
const sensitiveProperties = { ...fragment.sensitiveProperties };
|
|
1153
|
-
this
|
|
876
|
+
this.trackEvent({
|
|
1154
877
|
name,
|
|
1155
878
|
properties,
|
|
1156
879
|
sensitiveProperties,
|
|
1157
880
|
saveDataRecording: false,
|
|
1158
881
|
hasProperties: Object.keys(properties).length > 0 ||
|
|
1159
882
|
Object.keys(sensitiveProperties).length > 0,
|
|
1160
|
-
}, context
|
|
883
|
+
}, context);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Returns whether the current consent state allows analytics data to be
|
|
887
|
+
* captured, either for immediate delivery or to be held until the user
|
|
888
|
+
* decides.
|
|
889
|
+
*
|
|
890
|
+
* Capture is allowed once the user has opted in, and also while they are
|
|
891
|
+
* undecided if the pre-consent queue is enabled: what is captured then is
|
|
892
|
+
* replayed when they opt in (see {@link optIn}) and discarded if they opt out
|
|
893
|
+
* (see {@link optOut}). An explicit opt-out never allows capture.
|
|
894
|
+
*
|
|
895
|
+
* @returns True when analytics data may be captured.
|
|
896
|
+
*/
|
|
897
|
+
#isAnalyticsCaptureAllowed() {
|
|
898
|
+
if (analyticsControllerSelectors.selectEnabled(this.state)) {
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
return this.#isPreConsentQueueEnabled && !this.state.consentDecisionMade;
|
|
1161
902
|
}
|
|
1162
903
|
/**
|
|
1163
904
|
* Track an analytics event.
|
|
@@ -1168,19 +909,16 @@ export class AnalyticsController extends BaseController {
|
|
|
1168
909
|
* @param context - Optional platform-specific context forwarded to the platform adapter.
|
|
1169
910
|
*/
|
|
1170
911
|
trackEvent(event, context) {
|
|
1171
|
-
this.#trackEvent(event, context, this.#purposesFromName(event.name), this.#eventsConfigVersion);
|
|
1172
|
-
}
|
|
1173
|
-
#trackEvent(event, context, purposes, version) {
|
|
1174
912
|
// An event captured while the user is still undecided is held in the
|
|
1175
913
|
// pre-consent queue (see #sendOrQueueTrackEvent) instead of being
|
|
1176
914
|
// delivered, and replayed if they later opt in.
|
|
1177
|
-
if (!this.#
|
|
915
|
+
if (!this.#isAnalyticsCaptureAllowed()) {
|
|
1178
916
|
return;
|
|
1179
917
|
}
|
|
1180
918
|
// if event does not have properties, send event without properties
|
|
1181
919
|
// and return to prevent any additional processing
|
|
1182
920
|
if (!event.hasProperties) {
|
|
1183
|
-
this.#sendOrQueueTrackEvent(event.name, undefined, this.#withLocationContext(context)
|
|
921
|
+
this.#sendOrQueueTrackEvent(event.name, undefined, this.#withLocationContext(context));
|
|
1184
922
|
return;
|
|
1185
923
|
}
|
|
1186
924
|
// Track regular properties first if anonymous events feature is enabled
|
|
@@ -1189,7 +927,7 @@ export class AnalyticsController extends BaseController {
|
|
|
1189
927
|
// an event with user ID is tracked.
|
|
1190
928
|
this.#sendOrQueueTrackEvent(event.name, {
|
|
1191
929
|
...event.properties,
|
|
1192
|
-
}, this.#withLocationContext(context)
|
|
930
|
+
}, this.#withLocationContext(context));
|
|
1193
931
|
}
|
|
1194
932
|
const hasSensitiveProperties = Object.keys(event.sensitiveProperties).length > 0;
|
|
1195
933
|
if (!this.#isAnonymousEventsFeatureEnabled || hasSensitiveProperties) {
|
|
@@ -1203,7 +941,7 @@ export class AnalyticsController extends BaseController {
|
|
|
1203
941
|
// disabled, this is the single identified payload, so it is enriched.
|
|
1204
942
|
this.#isAnonymousEventsFeatureEnabled
|
|
1205
943
|
? context
|
|
1206
|
-
: this.#withLocationContext(context)
|
|
944
|
+
: this.#withLocationContext(context));
|
|
1207
945
|
}
|
|
1208
946
|
}
|
|
1209
947
|
/**
|
|
@@ -1216,6 +954,7 @@ export class AnalyticsController extends BaseController {
|
|
|
1216
954
|
if (!analyticsControllerSelectors.selectEnabled(this.state)) {
|
|
1217
955
|
return;
|
|
1218
956
|
}
|
|
957
|
+
// Delegate to platform adapter using the current analytics ID
|
|
1219
958
|
this.#sendOrQueueIdentifyEvent(this.state.analyticsId, traits, this.#withLocationContext(context));
|
|
1220
959
|
}
|
|
1221
960
|
/**
|
|
@@ -1226,13 +965,11 @@ export class AnalyticsController extends BaseController {
|
|
|
1226
965
|
* @param context - Optional platform-specific context forwarded to the platform adapter.
|
|
1227
966
|
*/
|
|
1228
967
|
trackView(name, properties, context) {
|
|
1229
|
-
|
|
1230
|
-
const version = this.#eventsConfigVersion;
|
|
1231
|
-
if (!this.#isCaptureAllowed(purposes)) {
|
|
968
|
+
if (!analyticsControllerSelectors.selectEnabled(this.state)) {
|
|
1232
969
|
return;
|
|
1233
970
|
}
|
|
1234
971
|
// Delegate to platform adapter
|
|
1235
|
-
this.#sendOrQueueViewEvent(name, properties, this.#withLocationContext(context)
|
|
972
|
+
this.#sendOrQueueViewEvent(name, properties, this.#withLocationContext(context));
|
|
1236
973
|
}
|
|
1237
974
|
/**
|
|
1238
975
|
* Create an event fragment.
|
|
@@ -1259,17 +996,11 @@ export class AnalyticsController extends BaseController {
|
|
|
1259
996
|
* Use {@link updateEventFragment} or {@link upsertEventFragment} to write.
|
|
1260
997
|
*/
|
|
1261
998
|
createEventFragment(options = {}) {
|
|
1262
|
-
|
|
1263
|
-
// affect the event-purpose snapshot.
|
|
1264
|
-
if (this.#shouldIgnoreEventFragmentCall('createEventFragment', {
|
|
1265
|
-
initialEvent: options.initialEvent,
|
|
1266
|
-
successEvent: options.successEvent,
|
|
1267
|
-
failureEvent: options.failureEvent,
|
|
1268
|
-
})) {
|
|
999
|
+
if (this.#shouldIgnoreEventFragmentCall('createEventFragment')) {
|
|
1269
1000
|
return undefined;
|
|
1270
1001
|
}
|
|
1271
1002
|
const now = Date.now();
|
|
1272
|
-
const fragment =
|
|
1003
|
+
const fragment = {
|
|
1273
1004
|
id: options.id ?? uuid(),
|
|
1274
1005
|
properties: { ...(options.properties ?? {}) },
|
|
1275
1006
|
sensitiveProperties: { ...(options.sensitiveProperties ?? {}) },
|
|
@@ -1288,7 +1019,8 @@ export class AnalyticsController extends BaseController {
|
|
|
1288
1019
|
? {}
|
|
1289
1020
|
: { context: { ...options.context } }),
|
|
1290
1021
|
...(options.persist === undefined ? {} : { persist: options.persist }),
|
|
1291
|
-
}
|
|
1022
|
+
};
|
|
1023
|
+
this.#setEventFragment(fragment);
|
|
1292
1024
|
if (fragment.initialEvent) {
|
|
1293
1025
|
this.#emitEventFragment(fragment, fragment.initialEvent, fragment.context);
|
|
1294
1026
|
}
|
|
@@ -1305,10 +1037,10 @@ export class AnalyticsController extends BaseController {
|
|
|
1305
1037
|
* @param payload - The properties and context to merge in.
|
|
1306
1038
|
*/
|
|
1307
1039
|
upsertEventFragment(id, payload = {}) {
|
|
1308
|
-
|
|
1309
|
-
if (this.#shouldIgnoreEventFragmentCall('upsertEventFragment', fragment ?? {})) {
|
|
1040
|
+
if (this.#shouldIgnoreEventFragmentCall('upsertEventFragment')) {
|
|
1310
1041
|
return;
|
|
1311
1042
|
}
|
|
1043
|
+
const fragment = this.#getEventFragment(id);
|
|
1312
1044
|
if (!fragment) {
|
|
1313
1045
|
this.createEventFragment({ id, ...payload });
|
|
1314
1046
|
return;
|
|
@@ -1326,10 +1058,10 @@ export class AnalyticsController extends BaseController {
|
|
|
1326
1058
|
* allow capture, the call is a logged no-op and does not throw.
|
|
1327
1059
|
*/
|
|
1328
1060
|
updateEventFragment(id, payload = {}) {
|
|
1329
|
-
|
|
1330
|
-
if (this.#shouldIgnoreEventFragmentCall('updateEventFragment', fragment)) {
|
|
1061
|
+
if (this.#shouldIgnoreEventFragmentCall('updateEventFragment')) {
|
|
1331
1062
|
return;
|
|
1332
1063
|
}
|
|
1064
|
+
const fragment = this.#getEventFragment(id);
|
|
1333
1065
|
if (!fragment) {
|
|
1334
1066
|
throw new Error(`Event fragment with id ${id} does not exist.`);
|
|
1335
1067
|
}
|
|
@@ -1346,10 +1078,10 @@ export class AnalyticsController extends BaseController {
|
|
|
1346
1078
|
* {@link upsertEventFragment} to write.
|
|
1347
1079
|
*/
|
|
1348
1080
|
getEventFragmentById(id) {
|
|
1349
|
-
|
|
1350
|
-
if (this.#shouldIgnoreEventFragmentCall('getEventFragmentById', fragment)) {
|
|
1081
|
+
if (this.#shouldIgnoreEventFragmentCall('getEventFragmentById')) {
|
|
1351
1082
|
return undefined;
|
|
1352
1083
|
}
|
|
1084
|
+
const fragment = this.#getEventFragment(id);
|
|
1353
1085
|
return fragment === undefined ? undefined : cloneDeep(fragment);
|
|
1354
1086
|
}
|
|
1355
1087
|
/**
|
|
@@ -1358,8 +1090,7 @@ export class AnalyticsController extends BaseController {
|
|
|
1358
1090
|
* @param id - The fragment ID.
|
|
1359
1091
|
*/
|
|
1360
1092
|
deleteEventFragment(id) {
|
|
1361
|
-
|
|
1362
|
-
if (this.#shouldIgnoreEventFragmentCall('deleteEventFragment', fragment)) {
|
|
1093
|
+
if (this.#shouldIgnoreEventFragmentCall('deleteEventFragment')) {
|
|
1363
1094
|
return;
|
|
1364
1095
|
}
|
|
1365
1096
|
this.#removeEventFragment(id);
|
|
@@ -1381,10 +1112,10 @@ export class AnalyticsController extends BaseController {
|
|
|
1381
1112
|
* allow capture, the call is a logged no-op and does not throw.
|
|
1382
1113
|
*/
|
|
1383
1114
|
finalizeEventFragment(id, { abandoned = false, context } = {}) {
|
|
1384
|
-
|
|
1385
|
-
if (this.#shouldIgnoreEventFragmentCall('finalizeEventFragment', fragment)) {
|
|
1115
|
+
if (this.#shouldIgnoreEventFragmentCall('finalizeEventFragment')) {
|
|
1386
1116
|
return;
|
|
1387
1117
|
}
|
|
1118
|
+
const fragment = this.#getEventFragment(id);
|
|
1388
1119
|
if (!fragment) {
|
|
1389
1120
|
throw new Error(`Event fragment with id ${id} does not exist.`);
|
|
1390
1121
|
}
|
|
@@ -1418,7 +1149,7 @@ export class AnalyticsController extends BaseController {
|
|
|
1418
1149
|
// consent decision may have changed while geolocation was resolving (e.g.
|
|
1419
1150
|
// resetConsentDecision ran during the await), and preserved pre-consent
|
|
1420
1151
|
// events must not be delivered once the user is no longer opted in.
|
|
1421
|
-
this.#
|
|
1152
|
+
this.#reconcilePreConsentEvents();
|
|
1422
1153
|
}
|
|
1423
1154
|
/**
|
|
1424
1155
|
* Opt out of analytics.
|
|
@@ -1432,7 +1163,9 @@ export class AnalyticsController extends BaseController {
|
|
|
1432
1163
|
state.optedIn = false;
|
|
1433
1164
|
state.consentDecisionMade = true;
|
|
1434
1165
|
});
|
|
1435
|
-
this.#
|
|
1166
|
+
this.#clearQueuedEvents();
|
|
1167
|
+
this.#clearPreConsentEvents();
|
|
1168
|
+
this.#clearEventFragments();
|
|
1436
1169
|
}
|
|
1437
1170
|
/**
|
|
1438
1171
|
* Reset the consent decision back to undecided.
|
|
@@ -1451,47 +1184,10 @@ export class AnalyticsController extends BaseController {
|
|
|
1451
1184
|
state.optedIn = false;
|
|
1452
1185
|
state.consentDecisionMade = false;
|
|
1453
1186
|
});
|
|
1454
|
-
this.#
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
*
|
|
1459
|
-
* Independent of {@link optIn}. Replays queued marketing events.
|
|
1460
|
-
*
|
|
1461
|
-
* @returns A promise that resolves once opt-in processing has completed.
|
|
1462
|
-
*/
|
|
1463
|
-
async optInToMarketing() {
|
|
1464
|
-
this.update((state) => {
|
|
1465
|
-
state.optedInToMarketing = true;
|
|
1466
|
-
state.marketingConsentDecisionMade = true;
|
|
1467
|
-
});
|
|
1468
|
-
await this.#maybeResolveLocation();
|
|
1469
|
-
this.#replayPreConsentEvents();
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Opt out of marketing analytics.
|
|
1473
|
-
*
|
|
1474
|
-
* Independent of {@link optOut}. Discards queued marketing events and
|
|
1475
|
-
* marketing event fragments.
|
|
1476
|
-
*/
|
|
1477
|
-
optOutOfMarketing() {
|
|
1478
|
-
this.update((state) => {
|
|
1479
|
-
state.optedInToMarketing = false;
|
|
1480
|
-
state.marketingConsentDecisionMade = true;
|
|
1481
|
-
});
|
|
1482
|
-
this.#pruneAllForConsent();
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* Reset the marketing consent decision back to undecided.
|
|
1486
|
-
*
|
|
1487
|
-
* Independent of {@link resetConsentDecision}.
|
|
1488
|
-
*/
|
|
1489
|
-
resetMarketingConsentDecision() {
|
|
1490
|
-
this.update((state) => {
|
|
1491
|
-
state.optedInToMarketing = false;
|
|
1492
|
-
state.marketingConsentDecisionMade = false;
|
|
1493
|
-
});
|
|
1494
|
-
this.#pruneAllForConsent();
|
|
1187
|
+
this.#clearQueuedEvents();
|
|
1188
|
+
if (!this.#isAnalyticsCaptureAllowed()) {
|
|
1189
|
+
this.#clearEventFragments();
|
|
1190
|
+
}
|
|
1495
1191
|
}
|
|
1496
1192
|
}
|
|
1497
1193
|
//# sourceMappingURL=AnalyticsController.js.map
|