@mparticle/web-sdk 3.21.0 → 3.23.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.
- package/dist/mparticle.common.js +230 -161
- package/dist/mparticle.esm.js +230 -161
- package/dist/mparticle.js +4536 -4111
- package/dist/types/src/batchUploader.d.ts +2 -0
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +2 -1
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +6 -0
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +7 -1
- package/dist/types/src/buttonTracking/controls.d.ts +3 -0
- package/dist/types/src/pageUrl.d.ts +10 -0
- package/dist/types/src/pageViewTracker.d.ts +2 -0
- package/dist/types/src/roktManager.d.ts +9 -2
- package/dist/types/src/sdkRuntimeModels.d.ts +2 -0
- package/package.json +1 -1
- package/src/batchUploader.ts +36 -3
- package/src/buttonTracking/buttonClickEvent.ts +3 -0
- package/src/buttonTracking/buttonDescription.ts +24 -3
- package/src/buttonTracking/buttonTracker.ts +92 -8
- package/src/buttonTracking/checkoutIntent.ts +199 -66
- package/src/buttonTracking/controls.ts +150 -32
- package/src/mp-instance.ts +5 -8
- package/src/mparticle-instance-manager.ts +3 -0
- package/src/pageUrl.ts +150 -0
- package/src/pageViewTracker.ts +60 -8
- package/src/roktManager.ts +50 -17
- package/src/sdkRuntimeModels.ts +2 -0
- package/src/serverModel.ts +3 -3
- package/src/utils.ts +1 -1
|
@@ -35,6 +35,8 @@ export declare class BatchUploader {
|
|
|
35
35
|
private exitHandler;
|
|
36
36
|
private visibilityChangeHandler;
|
|
37
37
|
private destroyed;
|
|
38
|
+
private batchOrder;
|
|
39
|
+
private nextBatchOrder;
|
|
38
40
|
/**
|
|
39
41
|
* Creates an instance of a BatchUploader
|
|
40
42
|
* @param {IMParticleWebSDKInstance} mpInstance - the mParticle SDK instance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ButtonElementKind = 'button' | 'input' | 'role' | 'wallet' | 'link';
|
|
2
2
|
export type LinkType = 'same_page' | 'outbound' | 'download';
|
|
3
|
-
export type ButtonLabelSource = 'labelledby' | 'aria' | 'text' | 'alt' | 'title' | 'value' | 'none';
|
|
3
|
+
export type ButtonLabelSource = 'labelledby' | 'aria' | 'text' | 'label' | 'alt' | 'title' | 'value' | 'none';
|
|
4
4
|
export interface IButtonClick {
|
|
5
5
|
label?: string;
|
|
6
6
|
labelSource: ButtonLabelSource;
|
|
@@ -16,5 +16,6 @@ export interface IButtonClick {
|
|
|
16
16
|
linkType?: LinkType;
|
|
17
17
|
linkDomain?: string;
|
|
18
18
|
fileExtension?: string;
|
|
19
|
+
checked?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export declare function describeButtonClick(control: Element): IButtonClick | undefined;
|
|
@@ -15,6 +15,9 @@ export declare class ButtonTracker {
|
|
|
15
15
|
private pagePath;
|
|
16
16
|
private pageEvents;
|
|
17
17
|
private pending;
|
|
18
|
+
private uploadTimerId;
|
|
19
|
+
private uploadHeld;
|
|
20
|
+
private leaving;
|
|
18
21
|
constructor(mpInstance: IMParticleWebSDKInstance);
|
|
19
22
|
get isActive(): boolean;
|
|
20
23
|
init(options: IButtonTrackerOptions): void;
|
|
@@ -28,6 +31,9 @@ export declare class ButtonTracker {
|
|
|
28
31
|
private readonly onEvent;
|
|
29
32
|
private observe;
|
|
30
33
|
private logClick;
|
|
34
|
+
private queuedForUpload;
|
|
35
|
+
private uploadNow;
|
|
36
|
+
private cancelUpload;
|
|
31
37
|
private emit;
|
|
32
38
|
private eventsEnabled;
|
|
33
39
|
private isKitSuppressed;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export type CheckoutIntentKind = 'commit' | 'wallet' | 'checkout_step' | 'frame';
|
|
2
|
+
export type CheckoutIntentSource = 'vendor' | 'label' | 'brand' | 'frame' | 'submit';
|
|
3
|
+
export interface ICheckoutIntent {
|
|
4
|
+
kind: Exclude<CheckoutIntentKind, 'frame'>;
|
|
5
|
+
source: CheckoutIntentSource;
|
|
6
|
+
}
|
|
7
|
+
export declare function classifyIntent(control: Element, pathname: string): ICheckoutIntent | undefined;
|
|
2
8
|
export declare function classifyIntentControl(control: Element, pathname: string): Exclude<CheckoutIntentKind, 'frame'> | undefined;
|
|
3
|
-
export declare function classifyIntentFrame(target: EventTarget | null, win: Window): 'frame' | undefined;
|
|
9
|
+
export declare function classifyIntentFrame(target: EventTarget | null, win: Window): 'wallet' | 'frame' | undefined;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const CONTROL_SELECTOR = "button, input[type=\"submit\"], input[type=\"button\"], input[type=\"image\"], [role=\"button\"]";
|
|
2
2
|
export declare function hasComposedAncestor(element: Element, selector: string): boolean;
|
|
3
3
|
export declare function isRoktElement(element: Element): boolean;
|
|
4
|
+
export declare function vendorButtonKind(control: Element): 'wallet' | 'checkout_step' | 'none' | undefined;
|
|
4
5
|
export declare function isWalletElement(element: Element): boolean;
|
|
6
|
+
export declare function isQuietIntentControl(control: Element, isVendorWallet: boolean): boolean;
|
|
5
7
|
export declare function findControl(path: Array<EventTarget | null>): Element | undefined;
|
|
6
8
|
export declare function isUnsafeTextSource(element: Element): boolean;
|
|
9
|
+
export declare function isUnsafeLabelSource(label: Element): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { valueof } from './utils';
|
|
2
|
+
export declare const UrlMode: {
|
|
3
|
+
readonly Path: "path";
|
|
4
|
+
readonly Full: "full";
|
|
5
|
+
};
|
|
6
|
+
export type UrlMode = valueof<typeof UrlMode>;
|
|
7
|
+
export declare const EXCLUDED_URL_QUERY_PARAMS: string[];
|
|
8
|
+
export declare const EXCLUDED_URL_FRAGMENT_PARAMS: string[];
|
|
9
|
+
export declare const cleanUrl: (href: string, mode: UrlMode) => string;
|
|
10
|
+
export declare const cleanPageHref: () => string;
|
|
@@ -3,6 +3,8 @@ import { BaseEvent } from './sdkRuntimeModels';
|
|
|
3
3
|
import { Dictionary } from './utils';
|
|
4
4
|
export declare const WIN_APV_KEY = "__mpApv__";
|
|
5
5
|
export declare const ALLOWED_QUERY_PARAMS: string[];
|
|
6
|
+
export declare const ALLOWED_QUERY_PARAM_PREFIXES: string[];
|
|
7
|
+
export declare const isAllowedQueryParam: (name: string) => boolean;
|
|
6
8
|
interface IPageSnapshot {
|
|
7
9
|
path: string;
|
|
8
10
|
params: Dictionary<string>;
|
|
@@ -5,10 +5,11 @@ import { Dictionary, AttributeValue } from "./utils";
|
|
|
5
5
|
import { SDKIdentityApi } from "./identity.interfaces";
|
|
6
6
|
import { SDKLoggerApi } from "./sdkRuntimeModels";
|
|
7
7
|
import { IStore, LocalSessionAttributes } from "./store";
|
|
8
|
+
import { MPID } from "@mparticle/web-sdk";
|
|
8
9
|
import { IErrorReportingService, ILoggingService } from "./reporting/types";
|
|
9
10
|
import { IRoktLauncherOptions } from "./roktLauncherOptions";
|
|
10
11
|
import IntegrationCapture from "./integrationCapture";
|
|
11
|
-
import type { CheckoutIntentKind } from "./buttonTracking/checkoutIntent";
|
|
12
|
+
import type { CheckoutIntentKind, CheckoutIntentSource } from "./buttonTracking/checkoutIntent";
|
|
12
13
|
export type RoktAttributeValueArray = Array<string | number | boolean>;
|
|
13
14
|
export type RoktAttributeValueType = string | number | boolean | undefined | null;
|
|
14
15
|
export type RoktAttributeValue = RoktAttributeValueType | RoktAttributeValueArray;
|
|
@@ -46,6 +47,8 @@ export interface RoktKitFilterSettings {
|
|
|
46
47
|
export interface ICheckoutIntentSignal {
|
|
47
48
|
kind: CheckoutIntentKind;
|
|
48
49
|
t: number;
|
|
50
|
+
source?: CheckoutIntentSource;
|
|
51
|
+
pathname?: string;
|
|
49
52
|
}
|
|
50
53
|
export interface IRoktKitSettings {
|
|
51
54
|
accountId?: string;
|
|
@@ -80,6 +83,7 @@ export default class RoktManager {
|
|
|
80
83
|
kit: IRoktKit;
|
|
81
84
|
filters: RoktKitFilterSettings;
|
|
82
85
|
private currentUser;
|
|
86
|
+
private createFilteredUser?;
|
|
83
87
|
private messageQueue;
|
|
84
88
|
private sandbox;
|
|
85
89
|
private placementAttributesMapping;
|
|
@@ -114,15 +118,17 @@ export default class RoktManager {
|
|
|
114
118
|
* @param {Function} captureTiming - Function to capture performance timing marks
|
|
115
119
|
* @param {IErrorReportingService} errorReporter - Dispatcher for error/warning reporting
|
|
116
120
|
* @param {ILoggingService} loggingService - Dispatcher for informational logging
|
|
121
|
+
* @param {Function} createFilteredUser - Builds a user as the Rokt connection's filters and the data plan allow
|
|
117
122
|
*
|
|
118
123
|
* @throws Logs error to console if placementAttributesMapping parsing fails
|
|
119
124
|
*/
|
|
120
|
-
init(roktConfig: IKitConfigs, filteredUser: IMParticleUser, identityService: SDKIdentityApi, store: IStore, logger?: SDKLoggerApi, options?: IRoktOptions, captureTiming?: (metricsName: string) => void, errorReporter?: IErrorReportingService, loggingService?: ILoggingService, integrationCapture?: IntegrationCapture): void;
|
|
125
|
+
init(roktConfig: IKitConfigs, filteredUser: IMParticleUser, identityService: SDKIdentityApi, store: IStore, logger?: SDKLoggerApi, options?: IRoktOptions, captureTiming?: (metricsName: string) => void, errorReporter?: IErrorReportingService, loggingService?: ILoggingService, integrationCapture?: IntegrationCapture, createFilteredUser?: (mpid: MPID) => IMParticleUser): void;
|
|
121
126
|
get isInitialized(): boolean;
|
|
122
127
|
attachKit(kit: IRoktKit): void;
|
|
123
128
|
private watchRouteChanges;
|
|
124
129
|
private notifyRouteChange;
|
|
125
130
|
isCheckoutIntentSuppressed(): boolean;
|
|
131
|
+
isCheckoutIntentLive(): boolean;
|
|
126
132
|
notifyCheckoutIntent(signal: ICheckoutIntentSignal, pathname?: string): void;
|
|
127
133
|
discardCheckoutIntents(): void;
|
|
128
134
|
private flushCheckoutIntents;
|
|
@@ -143,6 +149,7 @@ export default class RoktManager {
|
|
|
143
149
|
* });
|
|
144
150
|
*/
|
|
145
151
|
selectPlacements(options: IRoktSelectPlacementsOptions): Promise<IRoktSelection>;
|
|
152
|
+
private logFilteredIdentity;
|
|
146
153
|
/**
|
|
147
154
|
* Hashes attributes and returns both original and hashed versions
|
|
148
155
|
* with Rokt-compatible key names (like emailsha256, mobilesha256)
|
|
@@ -19,6 +19,7 @@ import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from '.
|
|
|
19
19
|
import Constants from './constants';
|
|
20
20
|
import RoktManager, { IRoktLauncherOptions } from './roktManager';
|
|
21
21
|
import { IConsoleLogger } from './logger';
|
|
22
|
+
import { UrlMode } from './pageUrl';
|
|
22
23
|
import { ErrorCodes, IErrorReportingService, ILoggingService } from './reporting/types';
|
|
23
24
|
export type SDKEventCustomFlags = Dictionary<any>;
|
|
24
25
|
export interface SDKEvent {
|
|
@@ -194,6 +195,7 @@ export interface IMParticleInstanceManager extends MParticleWebSDK {
|
|
|
194
195
|
_instances: Dictionary<IMParticleWebSDKInstance>;
|
|
195
196
|
_isTestEnv?: boolean;
|
|
196
197
|
_forceNoLocalStorage?: boolean;
|
|
198
|
+
_cleanUrl(href: string, mode: UrlMode): string;
|
|
197
199
|
config: SDKInitConfig;
|
|
198
200
|
isIOS?: boolean;
|
|
199
201
|
MPSideloadedKit: typeof MPSideloadedKit;
|
package/package.json
CHANGED
package/src/batchUploader.ts
CHANGED
|
@@ -3,7 +3,8 @@ import Constants from './constants';
|
|
|
3
3
|
import { SDKEvent, SDKEventCustomFlags } from './sdkRuntimeModels';
|
|
4
4
|
import { convertEvents } from './sdkToEventsApiConverter';
|
|
5
5
|
import { MessageType, EventType } from './types';
|
|
6
|
-
import { getRampNumber,
|
|
6
|
+
import { getRampNumber, isEmpty, obfuscateDevData } from './utils';
|
|
7
|
+
import { cleanPageHref } from './pageUrl';
|
|
7
8
|
import { SessionStorageVault, LocalStorageVault, StorageResult } from './vault';
|
|
8
9
|
import {
|
|
9
10
|
AsyncUploader,
|
|
@@ -52,6 +53,11 @@ export class BatchUploader {
|
|
|
52
53
|
private exitHandler: (() => void) | null = null;
|
|
53
54
|
private visibilityChangeHandler: (() => void) | null = null;
|
|
54
55
|
private destroyed = false;
|
|
56
|
+
// The order this page's uploads first picked each batch up in, by request
|
|
57
|
+
// id. Batches enter uploads in creation order, so this keeps that order for
|
|
58
|
+
// batches that share a millisecond.
|
|
59
|
+
private batchOrder = new Map<string, number>();
|
|
60
|
+
private nextBatchOrder = 0;
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
63
|
* Creates an instance of a BatchUploader
|
|
@@ -199,7 +205,7 @@ export class BatchUploader {
|
|
|
199
205
|
Debug: SDKConfig.isDevelopmentMode,
|
|
200
206
|
ActiveTimeOnSite: _timeOnSiteTimer?.getTimeInForeground() || 0,
|
|
201
207
|
TotalTimeOnSite: _Store.getTotalTimeOnSite?.() || 0,
|
|
202
|
-
PageUrl:
|
|
208
|
+
PageUrl: cleanPageHref() || null,
|
|
203
209
|
IsBackgroundAST: true
|
|
204
210
|
} as SDKEvent;
|
|
205
211
|
|
|
@@ -444,12 +450,27 @@ export class BatchUploader {
|
|
|
444
450
|
// Clear out pending batches to avoid any potential duplication
|
|
445
451
|
const batchesToUpload = this.batchesQueuedForProcessing;
|
|
446
452
|
this.batchesQueuedForProcessing = [];
|
|
453
|
+
batchesToUpload.forEach(batch => {
|
|
454
|
+
if (!this.batchOrder.has(batch.source_request_id)) {
|
|
455
|
+
this.batchOrder.set(batch.source_request_id, this.nextBatchOrder++);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
447
458
|
|
|
448
459
|
const batchesThatDidNotUpload = await this.uploadBatches(
|
|
449
460
|
batchesToUpload,
|
|
450
461
|
useBeacon
|
|
451
462
|
);
|
|
452
463
|
|
|
464
|
+
// Only a batch that will be retried needs its place kept.
|
|
465
|
+
const retriedIds = (batchesThatDidNotUpload || []).map(
|
|
466
|
+
batch => batch.source_request_id
|
|
467
|
+
);
|
|
468
|
+
batchesToUpload.forEach(batch => {
|
|
469
|
+
if (retriedIds.indexOf(batch.source_request_id) === -1) {
|
|
470
|
+
this.batchOrder.delete(batch.source_request_id);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
453
474
|
// Batches that do not successfully upload are added back to the process queue
|
|
454
475
|
// in the order they were created so that we can attempt re-transmission in
|
|
455
476
|
// the same sequence. This is to prevent any potential data corruption.
|
|
@@ -475,7 +496,19 @@ export class BatchUploader {
|
|
|
475
496
|
}
|
|
476
497
|
|
|
477
498
|
private storeBatchesQueuedForProcessing(): void {
|
|
478
|
-
|
|
499
|
+
// Another upload may have stored its failed batches while this one was
|
|
500
|
+
// in flight. Merge them in creation order, whichever upload finished
|
|
501
|
+
// first, so they are not overwritten and quota trimming drops the oldest.
|
|
502
|
+
const orderOf = (batch: Batch): number =>
|
|
503
|
+
this.batchOrder.get(batch.source_request_id) ?? -1;
|
|
504
|
+
const batches = [
|
|
505
|
+
...this.batchesQueuedForProcessing,
|
|
506
|
+
...(this.batchVault.retrieve() ?? []),
|
|
507
|
+
].sort(
|
|
508
|
+
(a, b) =>
|
|
509
|
+
orderOf(a) - orderOf(b) ||
|
|
510
|
+
(a.timestamp_unixtime_ms ?? 0) - (b.timestamp_unixtime_ms ?? 0)
|
|
511
|
+
);
|
|
479
512
|
|
|
480
513
|
// Nothing to persist: reset offline storage to an empty state.
|
|
481
514
|
if (isEmpty(batches)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isFunction } from '../utils';
|
|
2
2
|
import {
|
|
3
3
|
hasComposedAncestor,
|
|
4
|
+
isUnsafeLabelSource,
|
|
4
5
|
isUnsafeTextSource,
|
|
5
6
|
isWalletElement,
|
|
6
7
|
} from './controls';
|
|
@@ -19,6 +20,7 @@ export type ButtonLabelSource =
|
|
|
19
20
|
| 'labelledby'
|
|
20
21
|
| 'aria'
|
|
21
22
|
| 'text'
|
|
23
|
+
| 'label'
|
|
22
24
|
| 'alt'
|
|
23
25
|
| 'title'
|
|
24
26
|
| 'value'
|
|
@@ -39,6 +41,7 @@ export interface IButtonClick {
|
|
|
39
41
|
linkType?: LinkType;
|
|
40
42
|
linkDomain?: string;
|
|
41
43
|
fileExtension?: string;
|
|
44
|
+
checked?: boolean;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
function elementKind(control: Element): ButtonElementKind {
|
|
@@ -63,9 +66,10 @@ function nonBlank(value: string | null): string | undefined {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// The button's name as a screen reader would read it, simplified: the first of
|
|
66
|
-
// aria-labelledby, aria-label, own text (or an inner image's alt), title,
|
|
67
|
-
// inputs their value
|
|
68
|
-
//
|
|
69
|
+
// aria-labelledby, aria-label, own text (or an inner image's alt), title, for
|
|
70
|
+
// inputs their value, and for checkboxes and radios their label's text. The text
|
|
71
|
+
// sources are skipped for controls that hold form fields or more than four
|
|
72
|
+
// children, so a typed value is never read.
|
|
69
73
|
function accessibleName(
|
|
70
74
|
control: Element
|
|
71
75
|
): { raw?: string; source: ButtonLabelSource } {
|
|
@@ -108,6 +112,16 @@ function accessibleName(
|
|
|
108
112
|
// What the browser shows on an input without a value.
|
|
109
113
|
return { raw: type, source: 'value' };
|
|
110
114
|
}
|
|
115
|
+
} else if (type === 'checkbox' || type === 'radio') {
|
|
116
|
+
// The label the browser ties to the box, never the box's value.
|
|
117
|
+
const label = control.labels ? control.labels[0] : undefined;
|
|
118
|
+
const text =
|
|
119
|
+
label && !isUnsafeLabelSource(label) && label.textContent
|
|
120
|
+
? label.textContent.trim()
|
|
121
|
+
: '';
|
|
122
|
+
if (text) {
|
|
123
|
+
return { raw: text, source: 'label' };
|
|
124
|
+
}
|
|
111
125
|
}
|
|
112
126
|
} else if (readable) {
|
|
113
127
|
const text = textOf(control);
|
|
@@ -318,5 +332,12 @@ export function describeButtonClick(
|
|
|
318
332
|
linkType: link ? link.type : undefined,
|
|
319
333
|
linkDomain: link ? link.domain : undefined,
|
|
320
334
|
fileExtension: link ? link.fileExtension : undefined,
|
|
335
|
+
// The box as the click left it: the browser toggles it before the click
|
|
336
|
+
// reaches listeners, and puts it back if the click is cancelled.
|
|
337
|
+
checked:
|
|
338
|
+
control instanceof HTMLInputElement &&
|
|
339
|
+
(control.type === 'checkbox' || control.type === 'radio')
|
|
340
|
+
? control.checked
|
|
341
|
+
: undefined,
|
|
321
342
|
};
|
|
322
343
|
}
|
|
@@ -4,7 +4,8 @@ import { buildButtonClickEvent } from './buttonClickEvent';
|
|
|
4
4
|
import { describeButtonClick, IButtonClick } from './buttonDescription';
|
|
5
5
|
import {
|
|
6
6
|
CheckoutIntentKind,
|
|
7
|
-
|
|
7
|
+
CheckoutIntentSource,
|
|
8
|
+
classifyIntent,
|
|
8
9
|
classifyIntentFrame,
|
|
9
10
|
} from './checkoutIntent';
|
|
10
11
|
import { findControl } from './controls';
|
|
@@ -19,6 +20,9 @@ const PATH_LIMIT = 12;
|
|
|
19
20
|
// A ceiling, not a dedupe: a held Enter key or browser automation fires a
|
|
20
21
|
// trusted click per repeat, and each one is forwarded to every kit.
|
|
21
22
|
const PAGE_EVENT_LIMIT = 100;
|
|
23
|
+
// A click uploads at once. Taps that follow it this quickly are held and share
|
|
24
|
+
// one upload once they stop.
|
|
25
|
+
const UPLOAD_DEBOUNCE_MS = 100;
|
|
22
26
|
|
|
23
27
|
interface IBtnState {
|
|
24
28
|
tracker?: ButtonTracker;
|
|
@@ -77,7 +81,7 @@ const CAPTURE_PASSIVE: AddEventListenerOptions = {
|
|
|
77
81
|
const PASSIVE: AddEventListenerOptions = { passive: true };
|
|
78
82
|
|
|
79
83
|
// Logs every button click as an mParticle event, and hands checkout intent to the
|
|
80
|
-
// Rokt kit as a frozen {kind, t}. Submits and wallet frame focus feed the kit only.
|
|
84
|
+
// Rokt kit as a frozen {kind, t, source, pathname}. Submits and wallet frame focus feed the kit only.
|
|
81
85
|
export class ButtonTracker {
|
|
82
86
|
private readonly mpInstance: IMParticleWebSDKInstance;
|
|
83
87
|
private active = false;
|
|
@@ -90,6 +94,10 @@ export class ButtonTracker {
|
|
|
90
94
|
private pageEvents = 0;
|
|
91
95
|
// Clicks read at dispatch and waiting for the next paint.
|
|
92
96
|
private pending: Array<() => void> = [];
|
|
97
|
+
private uploadTimerId: ReturnType<typeof setTimeout> | null = null;
|
|
98
|
+
private uploadHeld = false;
|
|
99
|
+
// Set while the page is left, when the SDK's exit upload sends the queue.
|
|
100
|
+
private leaving = false;
|
|
93
101
|
|
|
94
102
|
constructor(mpInstance: IMParticleWebSDKInstance) {
|
|
95
103
|
this.mpInstance = mpInstance;
|
|
@@ -181,6 +189,7 @@ export class ButtonTracker {
|
|
|
181
189
|
this.lastSent = {};
|
|
182
190
|
this.pagePath = null;
|
|
183
191
|
this.pageEvents = 0;
|
|
192
|
+
this.cancelUpload();
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
private enqueue(delivery: () => void): void {
|
|
@@ -206,7 +215,14 @@ export class ButtonTracker {
|
|
|
206
215
|
event.type === 'pagehide' ||
|
|
207
216
|
document.visibilityState === 'hidden'
|
|
208
217
|
) {
|
|
209
|
-
this.
|
|
218
|
+
this.leaving = true;
|
|
219
|
+
try {
|
|
220
|
+
this.drain();
|
|
221
|
+
} finally {
|
|
222
|
+
this.leaving = false;
|
|
223
|
+
}
|
|
224
|
+
// The SDK's exit upload sends the queue, held taps included.
|
|
225
|
+
this.cancelUpload();
|
|
210
226
|
}
|
|
211
227
|
};
|
|
212
228
|
|
|
@@ -244,7 +260,9 @@ export class ButtonTracker {
|
|
|
244
260
|
// button that disables or relabels itself is seen as it was clicked.
|
|
245
261
|
// Logging and the kit hand-off wait for the next paint.
|
|
246
262
|
let click: IButtonClick | undefined;
|
|
263
|
+
let box: HTMLInputElement | undefined;
|
|
247
264
|
let kind: CheckoutIntentKind | undefined;
|
|
265
|
+
let source: CheckoutIntentSource | undefined;
|
|
248
266
|
let unconfirmedCommit = false;
|
|
249
267
|
if (layer === 'click') {
|
|
250
268
|
// The composed path is only populated during dispatch.
|
|
@@ -255,10 +273,15 @@ export class ButtonTracker {
|
|
|
255
273
|
);
|
|
256
274
|
if (control) {
|
|
257
275
|
click = logsEvents ? describeButtonClick(control) : undefined;
|
|
276
|
+
if (click && click.checked !== undefined) {
|
|
277
|
+
box = control as HTMLInputElement;
|
|
278
|
+
}
|
|
258
279
|
// Checkout intent is only worked out when the kit can use it.
|
|
259
|
-
|
|
260
|
-
?
|
|
280
|
+
const intent = signalsKit
|
|
281
|
+
? classifyIntent(control, pathname)
|
|
261
282
|
: undefined;
|
|
283
|
+
kind = intent ? intent.kind : undefined;
|
|
284
|
+
source = intent ? intent.source : undefined;
|
|
262
285
|
}
|
|
263
286
|
} else if (layer === 'submit') {
|
|
264
287
|
const submitter = (event as Event & { submitter?: Element | null })
|
|
@@ -268,8 +291,11 @@ export class ButtonTracker {
|
|
|
268
291
|
// commit click, which is known once that click is delivered.
|
|
269
292
|
if (!submitter) {
|
|
270
293
|
kind = 'commit';
|
|
294
|
+
source = 'submit';
|
|
271
295
|
} else if (control) {
|
|
272
|
-
|
|
296
|
+
const intent = classifyIntent(control, pathname);
|
|
297
|
+
kind = intent ? intent.kind : undefined;
|
|
298
|
+
source = intent ? intent.source : undefined;
|
|
273
299
|
}
|
|
274
300
|
unconfirmedCommit = !submitter;
|
|
275
301
|
}
|
|
@@ -284,6 +310,7 @@ export class ButtonTracker {
|
|
|
284
310
|
// Read after the blur settles: some browsers have not moved
|
|
285
311
|
// focus into the frame yet when the window blurs.
|
|
286
312
|
kind = classifyIntentFrame(document.activeElement, window);
|
|
313
|
+
source = 'frame';
|
|
287
314
|
} else if (
|
|
288
315
|
unconfirmedCommit &&
|
|
289
316
|
t - this.lastCommitClick > SUBMIT_AFTER_COMMIT_CLICK_MS
|
|
@@ -291,10 +318,21 @@ export class ButtonTracker {
|
|
|
291
318
|
kind = undefined;
|
|
292
319
|
}
|
|
293
320
|
if (click && logsEvents) {
|
|
321
|
+
// The box as this click left it. The browser toggles it
|
|
322
|
+
// before dispatch and puts it back once a cancelled click
|
|
323
|
+
// is over, so only a cancelled click changes the reading
|
|
324
|
+
// taken at dispatch: a checkbox to the other state, a
|
|
325
|
+
// radio to however it was restored.
|
|
326
|
+
if (box && event.defaultPrevented) {
|
|
327
|
+
click.checked =
|
|
328
|
+
box.type === 'checkbox'
|
|
329
|
+
? !click.checked
|
|
330
|
+
: box.checked;
|
|
331
|
+
}
|
|
294
332
|
this.logClick(click, pathname);
|
|
295
333
|
}
|
|
296
334
|
if (kind) {
|
|
297
|
-
this.emit(kind, layer, t, pathname);
|
|
335
|
+
this.emit(kind, source, layer, t, pathname);
|
|
298
336
|
}
|
|
299
337
|
} catch (e) {
|
|
300
338
|
this.log('[error] delivery threw: ' + e);
|
|
@@ -317,13 +355,59 @@ export class ButtonTracker {
|
|
|
317
355
|
this.pageEvents++;
|
|
318
356
|
// Restarts an expired session, as public logEvent() and page views do.
|
|
319
357
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
358
|
+
const queued = this.queuedForUpload();
|
|
320
359
|
this.mpInstance._Events.logEvent(
|
|
321
360
|
buildButtonClickEvent(click, pathname)
|
|
322
361
|
);
|
|
362
|
+
// Only a click waiting for the batch interval is sent early: a held
|
|
363
|
+
// one goes out when released, and with batching off it already has.
|
|
364
|
+
// A checkbox or radio waits for the batch, since choosing an option
|
|
365
|
+
// doesn't leave the page the way a confirm click can.
|
|
366
|
+
if (
|
|
367
|
+
!this.leaving &&
|
|
368
|
+
click.checked === undefined &&
|
|
369
|
+
this.queuedForUpload() > queued
|
|
370
|
+
) {
|
|
371
|
+
this.uploadNow();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Events waiting in the upload queue for the next batch interval.
|
|
376
|
+
private queuedForUpload(): number {
|
|
377
|
+
const uploader = this.mpInstance._APIClient.uploader;
|
|
378
|
+
return uploader ? uploader.eventsQueuedForProcessing.length : 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Uploads a click without waiting for the batch interval. A tap inside the
|
|
382
|
+
// wait after the last one is held, and each restarts the wait, so a burst
|
|
383
|
+
// after the first click goes out in one request once the taps stop.
|
|
384
|
+
private uploadNow(): void {
|
|
385
|
+
if (this.uploadTimerId === null) {
|
|
386
|
+
this.mpInstance.upload();
|
|
387
|
+
} else {
|
|
388
|
+
clearTimeout(this.uploadTimerId);
|
|
389
|
+
this.uploadHeld = true;
|
|
390
|
+
}
|
|
391
|
+
this.uploadTimerId = setTimeout(() => {
|
|
392
|
+
this.uploadTimerId = null;
|
|
393
|
+
if (this.uploadHeld) {
|
|
394
|
+
this.uploadHeld = false;
|
|
395
|
+
this.mpInstance.upload();
|
|
396
|
+
}
|
|
397
|
+
}, UPLOAD_DEBOUNCE_MS);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private cancelUpload(): void {
|
|
401
|
+
if (this.uploadTimerId !== null) {
|
|
402
|
+
clearTimeout(this.uploadTimerId);
|
|
403
|
+
this.uploadTimerId = null;
|
|
404
|
+
}
|
|
405
|
+
this.uploadHeld = false;
|
|
323
406
|
}
|
|
324
407
|
|
|
325
408
|
private emit(
|
|
326
409
|
kind: CheckoutIntentKind,
|
|
410
|
+
source: CheckoutIntentSource | undefined,
|
|
327
411
|
layer: string,
|
|
328
412
|
t: number,
|
|
329
413
|
pathname: string
|
|
@@ -342,7 +426,7 @@ export class ButtonTracker {
|
|
|
342
426
|
this.lastSent[kind] = { t, pathname };
|
|
343
427
|
|
|
344
428
|
this.mpInstance._RoktManager.notifyCheckoutIntent(
|
|
345
|
-
Object.freeze({ kind, t }),
|
|
429
|
+
Object.freeze({ kind, t, source, pathname }),
|
|
346
430
|
pathname
|
|
347
431
|
);
|
|
348
432
|
}
|