@corbado/observe 1.10.1 → 1.11.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/analysis/client.mjs +37 -1595
- package/dist/analysis/default-tracker.mjs +16 -0
- package/dist/analysis/index.mjs +29 -41
- package/dist/analysis/plugins/cookie-storage.mjs +11 -0
- package/dist/analysis/plugins/cross-origin-session.mjs +171 -0
- package/dist/analysis/plugins/probes.mjs +69 -0
- package/dist/analysis/plugins/raw-errors.mjs +8 -0
- package/dist/analysis/probe-lane.mjs +175 -0
- package/dist/analysis/queue.mjs +18 -141
- package/dist/analysis/session-keys.mjs +11 -0
- package/dist/analysis/tracker-core.mjs +1352 -0
- package/dist/analysis/tracker-features.mjs +80 -0
- package/dist/analysis/tracker-functions.mjs +103 -0
- package/dist/analysis/utils/appconfirmation-operation-full.mjs +5 -1
- package/dist/analysis/utils/captcha-operation-full.mjs +5 -1
- package/dist/analysis/utils/email-link-operation-full.mjs +9 -1
- package/dist/analysis/utils/email-otp-operation-full.mjs +5 -1
- package/dist/analysis/utils/form-operation-full.mjs +5 -1
- package/dist/analysis/utils/key-operation-full.mjs +9 -1
- package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +8 -1
- package/dist/analysis/utils/operation-full.mjs +4 -2
- package/dist/analysis/utils/passkey-deletion-operation-full.mjs +5 -1
- package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +5 -1
- package/dist/analysis/utils/passkey-login-operation-full.mjs +5 -1
- package/dist/analysis/utils/password-enrollment-operation-full.mjs +5 -1
- package/dist/analysis/utils/password-login-operation-full.mjs +5 -1
- package/dist/analysis/utils/provide-data-operation-full.mjs +5 -1
- package/dist/analysis/utils/sms-otp-operation-full.mjs +5 -1
- package/dist/analysis/utils/social-operation-login.mjs +5 -1
- package/dist/analysis/utils/sso-operation-login.mjs +5 -1
- package/dist/analysis/utils/totp-operation-full.mjs +5 -1
- package/dist/analysis/utils/trusted-device-operation-full.mjs +9 -1
- package/dist/analysis/version.mjs +1 -1
- package/dist/cdn/script.global.js +4 -4
- package/dist/index.d.mts +1366 -883
- package/dist/index.d.ts +1366 -883
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +1 -1
package/dist/analysis/client.mjs
CHANGED
|
@@ -3,39 +3,11 @@ import {
|
|
|
3
3
|
KeySigningOperationFull,
|
|
4
4
|
KeyRegistrationOperationFull
|
|
5
5
|
} from "./utils/key-operation-full.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { readUserAgent } from "./readers/device.mjs";
|
|
9
|
-
import { SharedWindowObserver, getCapturePageId } from "./low-events/shared-window-observer.mjs";
|
|
10
|
-
import {
|
|
11
|
-
AuthEventName
|
|
12
|
-
} from "./types.mjs";
|
|
13
|
-
import { FetchTransport } from "./transports/fetch.mjs";
|
|
14
|
-
import { RequestQueue } from "./queue.mjs";
|
|
15
|
-
import { DeviceCollector } from "./device.mjs";
|
|
16
|
-
import { DeviceInfoManager } from "./device-info-manager.mjs";
|
|
17
|
-
import {
|
|
18
|
-
CookieStorage,
|
|
19
|
-
LocalStorage,
|
|
20
|
-
namespacedKey,
|
|
21
|
-
NamespacedStorage,
|
|
22
|
-
SessionStorage
|
|
23
|
-
} from "./storage/index.mjs";
|
|
24
|
-
import { OUTBOX_LOCK_NAME } from "./store/event-store.mjs";
|
|
25
|
-
import { migrateLegacyStorage } from "./storage/migration.mjs";
|
|
26
|
-
import {
|
|
27
|
-
DEFAULT_RELIABILITY_CONFIG,
|
|
28
|
-
resolveFixedDataConfig,
|
|
29
|
-
resolveReliabilityConfig
|
|
30
|
-
} from "./config.mjs";
|
|
31
|
-
import { ConfigManager } from "./config-manager.mjs";
|
|
32
|
-
import { v7 as uuidv7 } from "uuid";
|
|
6
|
+
import { resolveFixedDataConfig } from "./config.mjs";
|
|
7
|
+
import { CorbadoTrackerCore } from "./tracker-core.mjs";
|
|
33
8
|
import { PasskeyLoginOperationFull } from "./utils/passkey-login-operation-full.mjs";
|
|
34
9
|
import { EmailOtpOperationFull } from "./utils/email-otp-operation-full.mjs";
|
|
35
10
|
import { SmsOtpOperationFull } from "./utils/sms-otp-operation-full.mjs";
|
|
36
|
-
import { isBlockedUserAgent } from "./utils/blocked-user-agents.mjs";
|
|
37
|
-
import { hasWebLocks, withWebLock } from "./readers/web-lock.mjs";
|
|
38
|
-
import { SDK_NAME, SDK_VERSION } from "./version.mjs";
|
|
39
11
|
import {
|
|
40
12
|
EmailLinkContinuationOperation,
|
|
41
13
|
EmailLinkOperationFull
|
|
@@ -43,7 +15,6 @@ import {
|
|
|
43
15
|
import {
|
|
44
16
|
PasskeyEnrollmentOperationFull
|
|
45
17
|
} from "./utils/passkey-enrollment-operation-full.mjs";
|
|
46
|
-
import { createLogger } from "./logger.mjs";
|
|
47
18
|
import { PasskeyDeletionOperationFull } from "./utils/passkey-deletion-operation-full.mjs";
|
|
48
19
|
import { PasswordLoginOperationFull } from "./utils/password-login-operation-full.mjs";
|
|
49
20
|
import { PasswordEnrollmentOperationFull } from "./utils/password-enrollment-operation-full.mjs";
|
|
@@ -61,272 +32,33 @@ import {
|
|
|
61
32
|
TrustedDeviceEnrollmentOperationFull
|
|
62
33
|
} from "./utils/trusted-device-operation-full.mjs";
|
|
63
34
|
import { ProvideDataOperationFull } from "./utils/provide-data-operation-full.mjs";
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
35
|
+
import { authDecisionFinished, authDecisionStarted, conversion, trackSubflowError } from "./tracker-functions.mjs";
|
|
36
|
+
import { clearExperimentOn, clearExperimentsOn, resetSessionOn, setFlowTagsOn, setUserOn } from "./tracker-features.mjs";
|
|
37
|
+
import { rawErrors } from "./plugins/raw-errors.mjs";
|
|
38
|
+
import { probe, probes } from "./plugins/probes.mjs";
|
|
39
|
+
import { cookieStorage } from "./plugins/cookie-storage.mjs";
|
|
40
|
+
import { crossOriginSession, getCrossOriginContext, setCrossOriginContext } from "./plugins/cross-origin-session.mjs";
|
|
41
|
+
function fullTrackerPlugins(options) {
|
|
42
|
+
const plugins = [rawErrors(), probes()];
|
|
43
|
+
if (options.storage === "cookie") {
|
|
44
|
+
plugins.push(cookieStorage({ cookieDomain: options.cookieDomain }));
|
|
45
|
+
}
|
|
46
|
+
const crossOrigin = resolveFixedDataConfig(
|
|
47
|
+
options.fixedDataConfig
|
|
48
|
+
)?.crossOriginSession;
|
|
49
|
+
if (crossOrigin) {
|
|
50
|
+
plugins.push(crossOriginSession(crossOrigin));
|
|
51
|
+
}
|
|
52
|
+
return plugins;
|
|
78
53
|
}
|
|
79
|
-
var
|
|
80
|
-
var SESSION_CONTINUITY_KEY = "cbo_session";
|
|
81
|
-
var SEQ_STORAGE_KEY = "cbo_seq";
|
|
82
|
-
var EXPERIMENTS_STORAGE_KEY = "cbo_experiments";
|
|
83
|
-
var DATA_POLICY_STORAGE_KEY = "cbo_data_policy";
|
|
84
|
-
var DATA_POLICY_MAX = 255;
|
|
85
|
-
var EXPERIMENT_KEY_RE = /^[A-Za-z0-9_-]+$/;
|
|
86
|
-
var EXPERIMENT_KEY_MAX = 128;
|
|
87
|
-
var EXPERIMENT_VALUE_MAX = 256;
|
|
88
|
-
var TELEMETRY_MESSAGE_MAX = 1e3;
|
|
89
|
-
var PROBE_KIND_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
90
|
-
var PROBE_ID_RE = /^[A-Za-z0-9_-]{1,16}$/;
|
|
91
|
-
var PROBE_CTX_MAX = 64;
|
|
92
|
-
var PROBE_PAGE_IDS_MAX = 32;
|
|
93
|
-
var MAX_SELF_TELEMETRY = 50;
|
|
94
|
-
var SEQ_LOCK_NAME = "cbo_seq";
|
|
95
|
-
var SESSION_ACTIVITY_WRITE_INTERVAL_MS = 1e3;
|
|
96
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
97
|
-
var CorbadoTracker = class _CorbadoTracker {
|
|
54
|
+
var CorbadoTracker = class extends CorbadoTrackerCore {
|
|
98
55
|
constructor(options) {
|
|
99
|
-
|
|
100
|
-
this.options.debug = options.debug ?? false;
|
|
101
|
-
this.logger = options.logger ?? createLogger({ debug: this.options.debug });
|
|
102
|
-
const localStorage = new LocalStorage(this.logger);
|
|
103
|
-
const sessionStorage = new SessionStorage(this.logger);
|
|
104
|
-
const isCookie = options.storage === "cookie";
|
|
105
|
-
const cookieStorage = isCookie ? new CookieStorage(this.logger, options.cookieDomain) : void 0;
|
|
106
|
-
migrateLegacyStorage(options.projectId, localStorage, sessionStorage, cookieStorage, this.logger);
|
|
107
|
-
const namespace = options.projectId;
|
|
108
|
-
const scoped = (engine) => new NamespacedStorage(engine, namespace);
|
|
109
|
-
this.seqLockName = namespacedKey(SEQ_LOCK_NAME, namespace);
|
|
110
|
-
this.sessionStorage = scoped(sessionStorage);
|
|
111
|
-
this.persistentStorage = scoped(localStorage);
|
|
112
|
-
this.storage = scoped(cookieStorage ?? localStorage);
|
|
113
|
-
this.configOverrides = {
|
|
114
|
-
...options.sdkConfig,
|
|
115
|
-
...options.sdkConfig?.retry && { retry: { ...options.sdkConfig.retry } },
|
|
116
|
-
...options.sdkConfig?.flushOnFlowTypeFinished && {
|
|
117
|
-
flushOnFlowTypeFinished: [...options.sdkConfig.flushOnFlowTypeFinished]
|
|
118
|
-
},
|
|
119
|
-
...options.sdkConfig?.sourcePathQueryAllow && {
|
|
120
|
-
sourcePathQueryAllow: [...options.sdkConfig.sourcePathQueryAllow]
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
this.fixedData = resolveFixedDataConfig(options.fixedDataConfig);
|
|
124
|
-
if (this.fixedData) {
|
|
125
|
-
this.configOverrides.pmd = this.fixedData.pmd;
|
|
126
|
-
}
|
|
127
|
-
const completeOverride = Object.keys(DEFAULT_RELIABILITY_CONFIG).every(
|
|
128
|
-
(key) => this.configOverrides[key] !== void 0
|
|
129
|
-
) && Object.keys(DEFAULT_RELIABILITY_CONFIG.retry).every(
|
|
130
|
-
(key) => this.configOverrides.retry?.[key] !== void 0
|
|
131
|
-
);
|
|
132
|
-
if (!completeOverride) {
|
|
133
|
-
this.configManager = new ConfigManager(
|
|
134
|
-
options.apiBaseUrl + (options.apiConfigPath ?? "/v1/observe/config") + "/" + options.projectId,
|
|
135
|
-
this.persistentStorage,
|
|
136
|
-
this.logger
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
const cachedConfig = this.configManager?.initialConfig;
|
|
140
|
-
this.config = resolveReliabilityConfig(cachedConfig, this.configOverrides);
|
|
141
|
-
if (!completeOverride && !cachedConfig && options.flushInterval != null && this.configOverrides.flushIntervalMs === void 0) {
|
|
142
|
-
this.config = { ...this.config, flushIntervalMs: options.flushInterval };
|
|
143
|
-
}
|
|
144
|
-
if (!completeOverride && !cachedConfig && options.deviceInfoCollectorTimeout != null && this.configOverrides.deviceInfoCollectorTimeoutMs === void 0) {
|
|
145
|
-
this.config = { ...this.config, deviceInfoCollectorTimeoutMs: options.deviceInfoCollectorTimeout };
|
|
146
|
-
}
|
|
147
|
-
if (options.apiBaseUrl !== "") {
|
|
148
|
-
if (!/^https?:\/\//.test(options.apiBaseUrl)) {
|
|
149
|
-
this.logger.critical(
|
|
150
|
-
`Invalid apiBaseUrl: URL must start with http:// or https:// (got: "${options.apiBaseUrl}")`
|
|
151
|
-
);
|
|
152
|
-
this.reportTelemetry("error", "invalid apiBaseUrl: missing http(s) scheme");
|
|
153
|
-
}
|
|
154
|
-
if (options.apiBaseUrl.endsWith("/")) {
|
|
155
|
-
this.logger.critical(`Invalid apiBaseUrl: URL must not have a trailing slash (got: "${options.apiBaseUrl}")`);
|
|
156
|
-
this.reportTelemetry("error", "invalid apiBaseUrl: trailing slash");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const url = options.apiBaseUrl + (options.apiEventPath ?? "/v1/observe/events") + "/" + options.projectId;
|
|
160
|
-
this.logger.debug(`Using API endpoint: ${url}`);
|
|
161
|
-
const transport = new FetchTransport({
|
|
162
|
-
logger: this.logger,
|
|
163
|
-
url
|
|
164
|
-
});
|
|
165
|
-
this.crossOrigin = this.createCrossOriginSession();
|
|
166
|
-
this.sessionId = this.resolveSession();
|
|
167
|
-
this.queue = new RequestQueue(
|
|
168
|
-
this.logger,
|
|
169
|
-
transport,
|
|
170
|
-
this.sessionId,
|
|
171
|
-
sdkInfo(this.options.integrationVersion, this.logger),
|
|
172
|
-
{
|
|
173
|
-
debug: this.options.debug,
|
|
174
|
-
config: this.config,
|
|
175
|
-
requestConfig: false,
|
|
176
|
-
outboxLockName: namespacedKey(OUTBOX_LOCK_NAME, namespace),
|
|
177
|
-
getDataPolicy: () => this.dataPolicy,
|
|
178
|
-
getCrossOriginSession: (sessionId) => this.crossOriginSessionMeta(sessionId),
|
|
179
|
-
fixedData: this.fixedData !== void 0 || typeof __CBO_DATA_PMD__ === "boolean" || typeof __CBO_DATA_PRIVATE_MODE__ === "boolean",
|
|
180
|
-
beforeEventBatchSend: (events) => this.attachUndeliveredDeviceInfo(events),
|
|
181
|
-
// The queue is created before the device info manager; both are only called once it exists.
|
|
182
|
-
standaloneDeviceInfo: {
|
|
183
|
-
has: () => this.deviceInfoManager?.hasStandalone() ?? false,
|
|
184
|
-
take: () => this.deviceInfoManager?.takeStandalone()
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
this.persistentStorage
|
|
188
|
-
);
|
|
189
|
-
this.telemetryReady = true;
|
|
190
|
-
this.logger.setTelemetrySink?.((level, message) => this.reportTelemetry(level, message));
|
|
191
|
-
try {
|
|
192
|
-
this.getWindowObserver();
|
|
193
|
-
} catch (error) {
|
|
194
|
-
this.logger.error("Failed to attach page low-event observer", error);
|
|
195
|
-
}
|
|
196
|
-
if (typeof __CBO_DATA_PMD__ === "boolean" ? __CBO_DATA_PMD__ : this.fixedData?.pmd !== false) {
|
|
197
|
-
try {
|
|
198
|
-
this.passwordManagerSampler = this.isTrackingBlocked() ? void 0 : new PasswordManagerSampler(
|
|
199
|
-
this.logger,
|
|
200
|
-
() => typeof __CBO_DATA_PMD__ === "boolean" ? __CBO_DATA_PMD__ : this.config.pmd,
|
|
201
|
-
this.getWindowObserver()
|
|
202
|
-
);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
this.logger.error("Failed to start password manager detection", error);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const bufferedTelemetry = this.pendingSelfTelemetry;
|
|
208
|
-
this.pendingSelfTelemetry = [];
|
|
209
|
-
for (const item of bufferedTelemetry) {
|
|
210
|
-
this.telemetry(item.level, item.message);
|
|
211
|
-
}
|
|
212
|
-
const deviceInfoCollector = new DeviceCollector(
|
|
213
|
-
this.logger,
|
|
214
|
-
this.storage,
|
|
215
|
-
isCookie ? "cookie" : "localstorage",
|
|
216
|
-
this.sessionStorage,
|
|
217
|
-
() => ({
|
|
218
|
-
timing: this.config.deviceInfoTiming
|
|
219
|
-
}),
|
|
220
|
-
// Timing entries carry raw millisecond values, so they go through the public telemetry path
|
|
221
|
-
// rather than reportTelemetry: the latter dedupes by exact message and is capped per load,
|
|
222
|
-
// where ever-changing messages would crowd out real error diagnostics.
|
|
223
|
-
(level, message) => this.telemetry(level, message),
|
|
224
|
-
this.fixedData?.privateMode ?? true
|
|
225
|
-
);
|
|
226
|
-
this.deviceCollector = deviceInfoCollector;
|
|
227
|
-
const deviceInfoDebounceTime = options.deviceInfoDebounceTime ?? 3e4;
|
|
228
|
-
this.deviceInfoManager = new DeviceInfoManager(
|
|
229
|
-
this.logger,
|
|
230
|
-
deviceInfoCollector,
|
|
231
|
-
Math.max(1, deviceInfoDebounceTime),
|
|
232
|
-
deviceInfoDebounceTime,
|
|
233
|
-
{
|
|
234
|
-
timing: () => this.config.deviceInfoTiming,
|
|
235
|
-
emit: (level, message) => this.telemetry(level, message)
|
|
236
|
-
},
|
|
237
|
-
() => this.queue.notifyDeviceInfo()
|
|
238
|
-
);
|
|
239
|
-
this.deviceInfoManager.start();
|
|
240
|
-
this.experiments = this.loadPersistedExperiments();
|
|
241
|
-
if (options.experiments) {
|
|
242
|
-
this.setExperiments(options.experiments);
|
|
243
|
-
}
|
|
244
|
-
this.dataPolicy = this.loadPersistedDataPolicy();
|
|
245
|
-
if (options.dataPolicy !== void 0) {
|
|
246
|
-
this.setDataPolicy(options.dataPolicy);
|
|
247
|
-
}
|
|
248
|
-
this.configManager?.start((config) => this.handleConfig(config));
|
|
56
|
+
super(options, fullTrackerPlugins(options));
|
|
249
57
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
storage;
|
|
254
|
-
sessionStorage;
|
|
255
|
-
/** Dedicated localStorage engine for reliability state (config cache, outbox, continuity session, seq). */
|
|
256
|
-
persistentStorage;
|
|
257
|
-
/** Seq/continuity Web Lock name; project-scoped together with the storage keys it guards. */
|
|
258
|
-
seqLockName;
|
|
259
|
-
/** Current resolved policy. Continuity and persistence stay enabled once activated within a document. */
|
|
260
|
-
config;
|
|
261
|
-
configManager;
|
|
262
|
-
configOverrides;
|
|
263
|
-
/** Resolved `fixedDataConfig`; undefined when not given (server-driven data switches). */
|
|
264
|
-
fixedData;
|
|
265
|
-
deviceInfoManager;
|
|
266
|
-
deviceCollector;
|
|
267
|
-
/**
|
|
268
|
-
* Cached per-tab id stamped onto every event's meta (see {@link EventMeta.tabId}). Resolved
|
|
269
|
-
* lazily from the collector's sessionStorage-backed id; stable for the whole page lifetime.
|
|
270
|
-
*/
|
|
271
|
-
eventTabId;
|
|
272
|
-
seq = 0;
|
|
273
|
-
/**
|
|
274
|
-
* When the continuity record's `lastActiveAt` was last persisted. Throttles that write inside the
|
|
275
|
-
* seq critical section (see {@link SESSION_ACTIVITY_WRITE_INTERVAL_MS}). 0 = not written yet on
|
|
276
|
-
* this load, so the first allocation always persists.
|
|
277
|
-
*/
|
|
278
|
-
lastActivityWriteAt = 0;
|
|
279
|
-
/** Persistent experiment assignments (experiment key → variant), attached to every produced event. */
|
|
280
|
-
experiments = {};
|
|
281
|
-
/**
|
|
282
|
-
* Persistent data policy code, stamped into every batch's `meta.dataPolicy` once set. Never
|
|
283
|
-
* cleared (only overwritten): it survives reloads, session rotation, resetSession and destroy.
|
|
284
|
-
*/
|
|
285
|
-
dataPolicy;
|
|
286
|
-
/** Self-instrumentation state: re-entrancy guard, readiness, per-load dedupe, and pre-queue buffer. */
|
|
287
|
-
telemetryReporting = false;
|
|
288
|
-
telemetryReady = false;
|
|
289
|
-
reportedTelemetryKeys = /* @__PURE__ */ new Set();
|
|
290
|
-
pendingSelfTelemetry = [];
|
|
291
|
-
/** Default probe page id (one per tracker instance) and the probe seq counter per page id. */
|
|
292
|
-
probePageId;
|
|
293
|
-
probeSeqs = /* @__PURE__ */ new Map();
|
|
294
|
-
windowObserver;
|
|
295
|
-
passwordManagerSampler;
|
|
296
|
-
/** Cross-origin session cookie (`fixedDataConfig.crossOriginSession`); undefined when off. */
|
|
297
|
-
crossOrigin;
|
|
298
|
-
/** The session this load took from the cross-origin cookie, if any (`meta.crossOriginSession`). */
|
|
299
|
-
crossOriginAdoptedId;
|
|
300
|
-
/** @internal Shared passive page observation and per-operation context registration. */
|
|
301
|
-
getWindowObserver() {
|
|
302
|
-
return this.windowObserver ??= new SharedWindowObserver(
|
|
303
|
-
this.logger,
|
|
304
|
-
(low, sessionId) => this.enqueueLowEvent(low, sessionId),
|
|
305
|
-
() => this.lowEventFlushKeepalive()
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
/** Apply init overrides on every refresh; continuity and persistence only activate within a document. */
|
|
309
|
-
handleConfig(config) {
|
|
310
|
-
const next = resolveReliabilityConfig(config, this.configOverrides);
|
|
311
|
-
next.sessionContinuity ||= this.config.sessionContinuity;
|
|
312
|
-
next.durableOutbox ||= this.config.durableOutbox;
|
|
313
|
-
this.config = next;
|
|
314
|
-
this.queue.updateConfig(this.config);
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Returns the logger used by this tracker.
|
|
318
|
-
* @internal
|
|
319
|
-
*/
|
|
320
|
-
getLogger() {
|
|
321
|
-
return this.logger;
|
|
322
|
-
}
|
|
323
|
-
applicationTag() {
|
|
324
|
-
if (this.options.applicationId) {
|
|
325
|
-
return { applicationId: this.options.applicationId.toLowerCase() };
|
|
326
|
-
}
|
|
327
|
-
return {};
|
|
58
|
+
/** @internal */
|
|
59
|
+
isFullTracker() {
|
|
60
|
+
return true;
|
|
328
61
|
}
|
|
329
|
-
// --- Experiments ---
|
|
330
62
|
/**
|
|
331
63
|
* Register the user's variant for an experiment (e.g. `setExperiment("button_color", "blue")`).
|
|
332
64
|
*
|
|
@@ -343,663 +75,13 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
343
75
|
setExperiment(key, variant) {
|
|
344
76
|
this.setExperiments({ [key]: variant });
|
|
345
77
|
}
|
|
346
|
-
/**
|
|
347
|
-
* Register several experiment assignments at once (experiment key → variant). Additive: existing
|
|
348
|
-
* assignments for other keys are kept. See {@link setExperiment}.
|
|
349
|
-
*/
|
|
350
|
-
setExperiments(assignments) {
|
|
351
|
-
try {
|
|
352
|
-
if (!assignments || typeof assignments !== "object") {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
let changed = false;
|
|
356
|
-
for (const [key, variant] of Object.entries(assignments)) {
|
|
357
|
-
if (!_CorbadoTracker.isValidExperimentKey(key) || !_CorbadoTracker.isValidExperimentVariant(variant)) {
|
|
358
|
-
this.logger.debug(`Ignoring invalid experiment assignment "${String(key)}"`);
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (this.experiments[key] !== variant) {
|
|
362
|
-
this.experiments[key] = variant;
|
|
363
|
-
changed = true;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (changed) {
|
|
367
|
-
this.persistExperiments();
|
|
368
|
-
}
|
|
369
|
-
} catch (error) {
|
|
370
|
-
this.logger.error("Failed to set experiments", error);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
78
|
/** Remove a single experiment assignment. */
|
|
374
79
|
clearExperiment(key) {
|
|
375
|
-
|
|
376
|
-
if (Object.prototype.hasOwnProperty.call(this.experiments, key)) {
|
|
377
|
-
delete this.experiments[key];
|
|
378
|
-
this.persistExperiments();
|
|
379
|
-
}
|
|
380
|
-
} catch (error) {
|
|
381
|
-
this.logger.error("Failed to clear experiment", error);
|
|
382
|
-
}
|
|
80
|
+
clearExperimentOn(this, key);
|
|
383
81
|
}
|
|
384
82
|
/** Remove all experiment assignments. */
|
|
385
83
|
clearExperiments() {
|
|
386
|
-
|
|
387
|
-
if (Object.keys(this.experiments).length === 0) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
this.experiments = {};
|
|
391
|
-
this.persistExperiments();
|
|
392
|
-
} catch (error) {
|
|
393
|
-
this.logger.error("Failed to clear experiments", error);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
/** Returns a copy of the currently active experiment assignments. */
|
|
397
|
-
getExperiments() {
|
|
398
|
-
return { ...this.experiments };
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Returns the currently active public SDK configuration, including live updates.
|
|
402
|
-
*
|
|
403
|
-
* @returns A frozen snapshot, created fresh on every call.
|
|
404
|
-
*/
|
|
405
|
-
/** @internal Project-scoped localStorage used by operation helpers (e.g. email-link continuity). */
|
|
406
|
-
getPersistentStorage() {
|
|
407
|
-
return this.persistentStorage;
|
|
408
|
-
}
|
|
409
|
-
getSdkConfig() {
|
|
410
|
-
return Object.freeze({
|
|
411
|
-
version: this.config.version,
|
|
412
|
-
aap: this.config.aap,
|
|
413
|
-
rawErrors: this.config.rawErrors
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
// --- Data policy ---
|
|
417
|
-
/**
|
|
418
|
-
* Set the data policy code for this tracker (e.g. `setDataPolicy(1)` once the user agreed to
|
|
419
|
-
* extended processing; `setDataPolicy(0)` to return to the project default). Codes are
|
|
420
|
-
* project-scoped non-negative integers (0..255) selecting a policy configured in Corbado; they
|
|
421
|
-
* carry no ordering.
|
|
422
|
-
*
|
|
423
|
-
* The code is persisted (localStorage, project-scoped) and sent as `meta.dataPolicy` on every
|
|
424
|
-
* subsequent batch, across reloads and session rotation. It is never cleared, only overwritten.
|
|
425
|
-
* The server records the latest code it receives for a session (last write wins). Invalid codes
|
|
426
|
-
* are ignored with a debug log; never throws.
|
|
427
|
-
*/
|
|
428
|
-
setDataPolicy(code) {
|
|
429
|
-
try {
|
|
430
|
-
if (!_CorbadoTracker.isValidDataPolicy(code)) {
|
|
431
|
-
this.logger.debug(`Ignoring invalid data policy code "${String(code)}"`);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
if (this.dataPolicy !== code) {
|
|
435
|
-
this.dataPolicy = code;
|
|
436
|
-
this.persistDataPolicy();
|
|
437
|
-
}
|
|
438
|
-
} catch (error) {
|
|
439
|
-
this.logger.error("Failed to set data policy", error);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
/** Returns the current data policy code, or undefined when none has ever been set. */
|
|
443
|
-
getDataPolicy() {
|
|
444
|
-
return this.dataPolicy;
|
|
445
|
-
}
|
|
446
|
-
static isValidDataPolicy(code) {
|
|
447
|
-
return typeof code === "number" && Number.isInteger(code) && code >= 0 && code <= DATA_POLICY_MAX;
|
|
448
|
-
}
|
|
449
|
-
loadPersistedDataPolicy() {
|
|
450
|
-
try {
|
|
451
|
-
const stored = this.persistentStorage.getItem(DATA_POLICY_STORAGE_KEY);
|
|
452
|
-
return _CorbadoTracker.isValidDataPolicy(stored) ? stored : void 0;
|
|
453
|
-
} catch (error) {
|
|
454
|
-
this.logger.debug("Failed to load persisted data policy", error);
|
|
455
|
-
return void 0;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
persistDataPolicy() {
|
|
459
|
-
try {
|
|
460
|
-
if (this.dataPolicy === void 0) {
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
this.persistentStorage.setItem(DATA_POLICY_STORAGE_KEY, this.dataPolicy);
|
|
464
|
-
} catch (error) {
|
|
465
|
-
this.logger.debug("Failed to persist data policy", error);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
static isValidExperimentKey(key) {
|
|
469
|
-
return typeof key === "string" && key.length > 0 && key.length <= EXPERIMENT_KEY_MAX && EXPERIMENT_KEY_RE.test(key);
|
|
470
|
-
}
|
|
471
|
-
static isValidExperimentVariant(variant) {
|
|
472
|
-
return typeof variant === "string" && variant.length > 0 && variant.length <= EXPERIMENT_VALUE_MAX;
|
|
473
|
-
}
|
|
474
|
-
loadPersistedExperiments() {
|
|
475
|
-
try {
|
|
476
|
-
const stored = this.persistentStorage.getItem(EXPERIMENTS_STORAGE_KEY);
|
|
477
|
-
if (!stored || typeof stored !== "object") {
|
|
478
|
-
return {};
|
|
479
|
-
}
|
|
480
|
-
const cleaned = {};
|
|
481
|
-
for (const [key, variant] of Object.entries(stored)) {
|
|
482
|
-
if (_CorbadoTracker.isValidExperimentKey(key) && _CorbadoTracker.isValidExperimentVariant(variant)) {
|
|
483
|
-
cleaned[key] = variant;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
return cleaned;
|
|
487
|
-
} catch (error) {
|
|
488
|
-
this.logger.debug("Failed to load persisted experiments", error);
|
|
489
|
-
return {};
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
persistExperiments() {
|
|
493
|
-
try {
|
|
494
|
-
this.persistentStorage.setItem(EXPERIMENTS_STORAGE_KEY, this.experiments);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
this.logger.debug("Failed to persist experiments", error);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Resolves the experiments to attach to one event: the persistent assignments with any per-call
|
|
501
|
-
* override merged on top. Returns undefined when there are none (so the field is omitted).
|
|
502
|
-
*/
|
|
503
|
-
resolveExperiments(perCall) {
|
|
504
|
-
const merged = { ...this.experiments };
|
|
505
|
-
if (perCall && typeof perCall === "object") {
|
|
506
|
-
for (const [key, variant] of Object.entries(perCall)) {
|
|
507
|
-
if (_CorbadoTracker.isValidExperimentKey(key) && _CorbadoTracker.isValidExperimentVariant(variant)) {
|
|
508
|
-
merged[key] = variant;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
513
|
-
}
|
|
514
|
-
isTrackingBlocked() {
|
|
515
|
-
if (typeof navigator === "undefined") {
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
return isBlockedUserAgent(
|
|
519
|
-
readUserAgent({
|
|
520
|
-
key: "browser-api:sdk.blocked-user-agent",
|
|
521
|
-
title: "User agent check for crawlers and bots",
|
|
522
|
-
accessNote: "navigator.userAgent, matched against a list of crawler and bot patterns",
|
|
523
|
-
group: "Observe SDK state",
|
|
524
|
-
origin: "observe-external",
|
|
525
|
-
fields: [
|
|
526
|
-
{
|
|
527
|
-
path: "result",
|
|
528
|
-
label: "Whether the browser looks like a bot",
|
|
529
|
-
exposure: "computed",
|
|
530
|
-
transform: { note: "matched against the blocked user-agent list; only the boolean is used (tracking off)" }
|
|
531
|
-
}
|
|
532
|
-
]
|
|
533
|
-
})
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
/** The cross-origin session cookie when `fixedDataConfig.crossOriginSession` is set and usable here. */
|
|
537
|
-
createCrossOriginSession() {
|
|
538
|
-
const option = this.fixedData?.crossOriginSession;
|
|
539
|
-
if (!option) {
|
|
540
|
-
return void 0;
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
if (this.isTrackingBlocked()) {
|
|
544
|
-
return void 0;
|
|
545
|
-
}
|
|
546
|
-
return CrossOriginSession.create({
|
|
547
|
-
projectId: this.options.projectId,
|
|
548
|
-
cookieDomain: option.cookieDomain,
|
|
549
|
-
logger: this.logger,
|
|
550
|
-
inactivityMs: () => this.config.sessionInactivityMs,
|
|
551
|
-
report: (level, message) => this.reportTelemetry(level, message)
|
|
552
|
-
});
|
|
553
|
-
} catch (error) {
|
|
554
|
-
this.logger.error("Failed to set up the cross-origin session", error);
|
|
555
|
-
return void 0;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Last activity of the local session records BEFORE this load rewrites them (both paths, by id), so the
|
|
560
|
-
* cross-origin boot can tell a session that was active here from one this load just created.
|
|
561
|
-
*/
|
|
562
|
-
readLocalSessionActivity() {
|
|
563
|
-
const activity = /* @__PURE__ */ new Map();
|
|
564
|
-
try {
|
|
565
|
-
const continuity = this.persistentStorage.getItem(SESSION_CONTINUITY_KEY);
|
|
566
|
-
if (continuity?.id && typeof continuity.lastActiveAt === "number") {
|
|
567
|
-
activity.set(continuity.id, continuity.lastActiveAt);
|
|
568
|
-
}
|
|
569
|
-
const perTab = this.sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
570
|
-
if (perTab && typeof perTab === "object" && perTab.id && typeof perTab.lastActiveAt === "number") {
|
|
571
|
-
activity.set(perTab.id, Math.max(activity.get(perTab.id) ?? 0, perTab.lastActiveAt));
|
|
572
|
-
}
|
|
573
|
-
} catch (error) {
|
|
574
|
-
this.logger.error("Failed to read local session activity", error);
|
|
575
|
-
}
|
|
576
|
-
return activity;
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Reconcile the session this load resolved locally with the cross-origin cookie before the first event:
|
|
580
|
-
* adopt the cookie's session (and persist it locally, so reloads and same-origin tabs follow) or keep
|
|
581
|
-
* the local one, and continue the seq at or above the cookie's mark. Never throws.
|
|
582
|
-
*/
|
|
583
|
-
bootCrossOriginSession(localActivity) {
|
|
584
|
-
try {
|
|
585
|
-
this.crossOriginAdoptedId = void 0;
|
|
586
|
-
const now = Date.now();
|
|
587
|
-
const continuity = this.config.sessionContinuity;
|
|
588
|
-
const localSeq = continuity ? Math.max(this.seq, this.persistentStorage.getItem(SEQ_STORAGE_KEY) ?? 0) : this.seq;
|
|
589
|
-
const result = this.crossOrigin?.boot(
|
|
590
|
-
{
|
|
591
|
-
sessionId: this.sessionId,
|
|
592
|
-
lastActiveAt: this.sessionId && localActivity.get(this.sessionId) || 0,
|
|
593
|
-
seq: localSeq
|
|
594
|
-
},
|
|
595
|
-
now
|
|
596
|
-
);
|
|
597
|
-
if (!result || !result.sessionId) {
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
if (result.adopted) {
|
|
601
|
-
this.sessionId = result.sessionId;
|
|
602
|
-
this.crossOriginAdoptedId = result.sessionId;
|
|
603
|
-
if (continuity) {
|
|
604
|
-
this.persistContinuitySession(result.sessionId, now);
|
|
605
|
-
} else {
|
|
606
|
-
this.persistPerTabSession(result.sessionId, now);
|
|
607
|
-
}
|
|
608
|
-
this.logger.debug("Adopted cross-origin session id", result.sessionId);
|
|
609
|
-
}
|
|
610
|
-
this.seq = result.seq;
|
|
611
|
-
if (continuity) {
|
|
612
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, result.seq);
|
|
613
|
-
}
|
|
614
|
-
} catch (error) {
|
|
615
|
-
this.logger.error("Failed to apply the cross-origin session", error);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Resolve the session of this load: locally ({@link resolveSessionId}), then reconciled with the
|
|
620
|
-
* cross-origin cookie when that option is on. Used at boot and by the recovery in `getSessionId`.
|
|
621
|
-
*/
|
|
622
|
-
resolveSession() {
|
|
623
|
-
if (!this.crossOrigin) {
|
|
624
|
-
return this.resolveSessionId();
|
|
625
|
-
}
|
|
626
|
-
const localActivity = this.readLocalSessionActivity();
|
|
627
|
-
this.sessionId = this.resolveSessionId();
|
|
628
|
-
this.bootCrossOriginSession(localActivity);
|
|
629
|
-
return this.sessionId;
|
|
630
|
-
}
|
|
631
|
-
/** `meta.crossOriginSession` of a batch of `sessionId`; undefined while the option is off on this page. */
|
|
632
|
-
crossOriginSessionMeta(sessionId) {
|
|
633
|
-
if (!this.crossOrigin?.isEnabled()) {
|
|
634
|
-
return void 0;
|
|
635
|
-
}
|
|
636
|
-
return sessionId && sessionId === this.crossOriginAdoptedId ? "adopted" : "own";
|
|
637
|
-
}
|
|
638
|
-
resolveSessionId() {
|
|
639
|
-
try {
|
|
640
|
-
if (this.config.sessionContinuity) {
|
|
641
|
-
return this.getContinuitySessionId();
|
|
642
|
-
}
|
|
643
|
-
const now = Date.now();
|
|
644
|
-
const stored = this.readPerTabSession();
|
|
645
|
-
if (stored && now - stored.lastActiveAt < this.perTabInactivityMs(stored)) {
|
|
646
|
-
this.persistPerTabSession(stored.id, now);
|
|
647
|
-
this.seq = 0;
|
|
648
|
-
return stored.id;
|
|
649
|
-
}
|
|
650
|
-
const newSessionId = uuidv7();
|
|
651
|
-
this.persistPerTabSession(newSessionId, now);
|
|
652
|
-
this.seq = 0;
|
|
653
|
-
this.logger.debug("Created session id", newSessionId);
|
|
654
|
-
return newSessionId;
|
|
655
|
-
} catch (error) {
|
|
656
|
-
this.logger.error("Failed to get session id", error);
|
|
657
|
-
this.reportTelemetry("error", "failed to get session id");
|
|
658
|
-
return "";
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Resolve the session id from localStorage so it survives reloads and is shared across tabs of the
|
|
663
|
-
* same browser. Rotates after `config.sessionInactivityMs` of inactivity (server-controlled,
|
|
664
|
-
* evaluated at boot), resetting the seq counter.
|
|
665
|
-
*
|
|
666
|
-
* Inactivity rotation happens at boot only, here and on the per-tab path {@link resolveSessionId}
|
|
667
|
-
* takes without continuity. Boot is chosen on an assumption, not a guarantee: that a load
|
|
668
|
-
* re-announces its flow, so the fresh session id gets a classifiable flow. That holds where a
|
|
669
|
-
* flow is opened per load. Where one is opened once and carried across loads it does not, and a
|
|
670
|
-
* boot rotation splits that flow — such a project needs a window long enough to span the journey.
|
|
671
|
-
* Mid-page the assumption is not even available: nothing re-announces between two tracked events,
|
|
672
|
-
* so rotating there would leave the started flow unfinished in the old session and the remaining
|
|
673
|
-
* events without a flow start in the new one — one gap counted as a drop-off, one completion
|
|
674
|
-
* counted as nothing. `nextSeq` therefore never rotates on inactivity, on either path.
|
|
675
|
-
*/
|
|
676
|
-
getContinuitySessionId() {
|
|
677
|
-
const now = Date.now();
|
|
678
|
-
const existing = this.persistentStorage.getItem(SESSION_CONTINUITY_KEY);
|
|
679
|
-
if (existing?.id && now - existing.lastActiveAt < this.config.sessionInactivityMs) {
|
|
680
|
-
this.persistContinuitySession(existing.id, now);
|
|
681
|
-
this.seq = this.persistentStorage.getItem(SEQ_STORAGE_KEY) ?? 0;
|
|
682
|
-
return existing.id;
|
|
683
|
-
}
|
|
684
|
-
if (!existing?.id) {
|
|
685
|
-
const perTab = this.readPerTabSession();
|
|
686
|
-
if (perTab && now - perTab.lastActiveAt < this.config.sessionInactivityMs) {
|
|
687
|
-
this.persistContinuitySession(perTab.id, now);
|
|
688
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, 0);
|
|
689
|
-
this.seq = 0;
|
|
690
|
-
this.logger.debug("Adopted per-tab session id as continuity session", perTab.id);
|
|
691
|
-
return perTab.id;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const newSessionId = uuidv7();
|
|
695
|
-
this.persistContinuitySession(newSessionId, now);
|
|
696
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, 0);
|
|
697
|
-
this.seq = 0;
|
|
698
|
-
this.logger.debug("Created continuity session id", newSessionId);
|
|
699
|
-
return newSessionId;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Persist the continuity record and remember when, so the throttled refresh in the seq critical
|
|
703
|
-
* section measures from the last write that actually happened — whoever made it.
|
|
704
|
-
*/
|
|
705
|
-
persistContinuitySession(id, now) {
|
|
706
|
-
this.lastActivityWriteAt = now;
|
|
707
|
-
this.persistentStorage.setItem(SESSION_CONTINUITY_KEY, { id, lastActiveAt: now });
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Read the per-tab session record. A bare string is the format that predates dating: it carries no
|
|
711
|
-
* `lastActiveAt`, so its window can only start on the load that reads it. That load rewrites it in
|
|
712
|
-
* the dated shape, so a tab converts once and ages normally from then on, and the case drains as
|
|
713
|
-
* the rollout replaces the bundles that write it.
|
|
714
|
-
*/
|
|
715
|
-
readPerTabSession() {
|
|
716
|
-
const stored = this.sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
717
|
-
if (!stored) {
|
|
718
|
-
return void 0;
|
|
719
|
-
}
|
|
720
|
-
if (typeof stored === "string") {
|
|
721
|
-
return UUID_RE.test(stored) ? { id: stored, lastActiveAt: Date.now() } : void 0;
|
|
722
|
-
}
|
|
723
|
-
if (typeof stored.lastActiveAt !== "number") {
|
|
724
|
-
return void 0;
|
|
725
|
-
}
|
|
726
|
-
return UUID_RE.test(stored.id) ? stored : void 0;
|
|
727
|
-
}
|
|
728
|
-
/**
|
|
729
|
-
* Persist the per-tab record and remember when, sharing the throttle bookkeeping with the
|
|
730
|
-
* continuity record — a load resolves its session on one path or the other, never both.
|
|
731
|
-
*
|
|
732
|
-
* An explicit timeout override or server config stamps the window. Otherwise carry forward what
|
|
733
|
-
* the tab already knows, so a defaults boot does not replace a previously configured threshold.
|
|
734
|
-
*/
|
|
735
|
-
persistPerTabSession(id, now) {
|
|
736
|
-
this.lastActivityWriteAt = now;
|
|
737
|
-
const inactivityMs = this.configOverrides.sessionInactivityMs !== void 0 || this.config.version ? this.config.sessionInactivityMs : this.readPerTabSession()?.inactivityMs;
|
|
738
|
-
this.sessionStorage.setItem(SESSION_STORAGE_KEY, {
|
|
739
|
-
id,
|
|
740
|
-
lastActiveAt: now,
|
|
741
|
-
...inactivityMs === void 0 ? {} : { inactivityMs }
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
/** Explicit init timeout wins; otherwise retain the window a prior configured load recorded. */
|
|
745
|
-
perTabInactivityMs(stored) {
|
|
746
|
-
return this.configOverrides.sessionInactivityMs !== void 0 ? this.config.sessionInactivityMs : stored.inactivityMs ?? this.config.sessionInactivityMs;
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
|
-
* Return the next sequence number. With session continuity the counter is persisted so ordering
|
|
750
|
-
* survives reloads/redirects; the read-increment-write runs under a cross-tab Web Lock
|
|
751
|
-
* ({@link SEQ_LOCK_NAME}) so two tabs sharing the session cannot allocate the same seq (where Web
|
|
752
|
-
* Locks are unavailable or exceed the configured timeout it degrades to the historical best-effort merge).
|
|
753
|
-
* Lock grants are FIFO, so seq order matches `track()` call order within a tab. Tracking also keeps the
|
|
754
|
-
* continuity session's `lastActiveAt` fresh (throttled, see {@link SESSION_ACTIVITY_WRITE_INTERVAL_MS}),
|
|
755
|
-
* so the window the NEXT boot evaluates measures real user
|
|
756
|
-
* inactivity instead of time-since-page-load (a long-lived active tab must not rotate the session
|
|
757
|
-
* on the next reload). It never rotates the session itself: the inactivity window is evaluated at
|
|
758
|
-
* boot only ({@link getContinuitySessionId} documents why). Consequence: while a page is never
|
|
759
|
-
* reloaded — foreground idle, bfcache restore, long-lived SPA tab — a session can outlive
|
|
760
|
-
* `sessionInactivityMs`. That inflates flow durations but keeps flow counts correct, which is the
|
|
761
|
-
* better half of the trade. An empty store while this load already has a session id means
|
|
762
|
-
* persistence is failing (or was cleared); the in-memory id is reused then — never re-minted — so
|
|
763
|
-
* a broken store degrades to one session per page load instead of one session per event.
|
|
764
|
-
*/
|
|
765
|
-
nextSeq() {
|
|
766
|
-
if (!this.config.sessionContinuity) {
|
|
767
|
-
const now = Date.now();
|
|
768
|
-
if (this.sessionId && now - this.lastActivityWriteAt >= SESSION_ACTIVITY_WRITE_INTERVAL_MS) {
|
|
769
|
-
this.persistPerTabSession(this.sessionId, now);
|
|
770
|
-
}
|
|
771
|
-
const current = this.crossOrigin ? this.crossOrigin.claim(this.sessionId, this.seq, now) : this.seq;
|
|
772
|
-
this.seq = current + 1;
|
|
773
|
-
return current;
|
|
774
|
-
}
|
|
775
|
-
const allocate = () => {
|
|
776
|
-
const now = Date.now();
|
|
777
|
-
const storedSession = this.persistentStorage.getItem(SESSION_CONTINUITY_KEY);
|
|
778
|
-
let sessionChanged = false;
|
|
779
|
-
if (!storedSession?.id && this.sessionId) {
|
|
780
|
-
this.persistContinuitySession(this.sessionId, now);
|
|
781
|
-
} else if (!storedSession?.id) {
|
|
782
|
-
const newSessionId = uuidv7();
|
|
783
|
-
this.persistContinuitySession(newSessionId, now);
|
|
784
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, 0);
|
|
785
|
-
this.sessionId = newSessionId;
|
|
786
|
-
this.queue.setSessionId(newSessionId);
|
|
787
|
-
this.seq = 0;
|
|
788
|
-
sessionChanged = true;
|
|
789
|
-
} else {
|
|
790
|
-
if (now - this.lastActivityWriteAt >= SESSION_ACTIVITY_WRITE_INTERVAL_MS) {
|
|
791
|
-
this.persistContinuitySession(storedSession.id, now);
|
|
792
|
-
}
|
|
793
|
-
if (storedSession.id !== this.sessionId) {
|
|
794
|
-
this.sessionId = storedSession.id;
|
|
795
|
-
this.queue.setSessionId(storedSession.id);
|
|
796
|
-
sessionChanged = true;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
const stored = this.persistentStorage.getItem(SEQ_STORAGE_KEY);
|
|
800
|
-
if (sessionChanged) {
|
|
801
|
-
this.seq = typeof stored === "number" ? stored : 0;
|
|
802
|
-
}
|
|
803
|
-
let current = typeof stored === "number" && stored > this.seq ? stored : this.seq;
|
|
804
|
-
if (this.crossOrigin) {
|
|
805
|
-
current = this.crossOrigin.claim(this.sessionId, current, now);
|
|
806
|
-
}
|
|
807
|
-
this.seq = current + 1;
|
|
808
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, this.seq);
|
|
809
|
-
return current;
|
|
810
|
-
};
|
|
811
|
-
const timeoutMs = this.config.seqLockTimeoutMs;
|
|
812
|
-
if (timeoutMs === 0 || !hasWebLocks({
|
|
813
|
-
key: "browser-api:sdk.web-locks",
|
|
814
|
-
title: "Coordination of Observe data across open tabs",
|
|
815
|
-
accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
|
|
816
|
-
group: "Observe SDK state",
|
|
817
|
-
origin: "observe-internal",
|
|
818
|
-
fields: [
|
|
819
|
-
{
|
|
820
|
-
path: "available",
|
|
821
|
-
label: "Whether the browser supports tab coordination",
|
|
822
|
-
exposure: "enum",
|
|
823
|
-
transform: { values: [true, false] }
|
|
824
|
-
}
|
|
825
|
-
]
|
|
826
|
-
})) {
|
|
827
|
-
return allocate();
|
|
828
|
-
}
|
|
829
|
-
return withWebLock(
|
|
830
|
-
{
|
|
831
|
-
key: "browser-api:sdk.web-locks",
|
|
832
|
-
title: "Coordination of Observe data across open tabs",
|
|
833
|
-
accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
|
|
834
|
-
group: "Observe SDK state",
|
|
835
|
-
origin: "observe-internal",
|
|
836
|
-
fields: [
|
|
837
|
-
{
|
|
838
|
-
path: "result",
|
|
839
|
-
label: "Coordination between open tabs",
|
|
840
|
-
exposure: "none",
|
|
841
|
-
transform: { note: "own cbo_* lock names; serializes the SDK's storage updates across tabs" }
|
|
842
|
-
}
|
|
843
|
-
]
|
|
844
|
-
},
|
|
845
|
-
this.seqLockName,
|
|
846
|
-
allocate,
|
|
847
|
-
{
|
|
848
|
-
timeoutMs,
|
|
849
|
-
onTimeout: () => {
|
|
850
|
-
this.logger.warn(`Sequence Web Lock acquisition timed out after ${timeoutMs}ms; using fallback`);
|
|
851
|
-
this.reportTelemetry("info", `seq web lock timeout; using fallback, ${timeoutMs}ms`);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
resolveTrackingSourcePath() {
|
|
857
|
-
if (typeof window === "undefined") {
|
|
858
|
-
return void 0;
|
|
859
|
-
}
|
|
860
|
-
return readSourcePath(
|
|
861
|
-
{
|
|
862
|
-
key: "page:location.source-path",
|
|
863
|
-
title: "Page path and allowed query parameters",
|
|
864
|
-
accessNote: "location.pathname and location.search; the query is reduced by the sourcePathQuery policy inside the reader (trackingSourcePath)",
|
|
865
|
-
group: "Page",
|
|
866
|
-
origin: "observe-external",
|
|
867
|
-
fields: [
|
|
868
|
-
{ path: "url.path", label: "Page path", exposure: "raw" },
|
|
869
|
-
{
|
|
870
|
-
path: "url.query",
|
|
871
|
-
label: "Page address parameters (allowed ones only)",
|
|
872
|
-
exposure: "redacted",
|
|
873
|
-
transform: { note: "sourcePathQuery 'allowlist': values of non-allowlisted keys become '*'; 'all': raw" }
|
|
874
|
-
}
|
|
875
|
-
]
|
|
876
|
-
},
|
|
877
|
-
this.config.sourcePathQuery,
|
|
878
|
-
this.config.sourcePathQueryAllow
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
/**
|
|
882
|
-
* Per-tab id for event meta, cached after the first successful resolution: the id is
|
|
883
|
-
* sessionStorage-backed and stable for the whole page lifetime.
|
|
884
|
-
*/
|
|
885
|
-
resolveEventTabId() {
|
|
886
|
-
if (this.eventTabId === void 0) {
|
|
887
|
-
this.eventTabId = this.deviceCollector.getTabId();
|
|
888
|
-
}
|
|
889
|
-
return this.eventTabId;
|
|
890
|
-
}
|
|
891
|
-
/** @internal */
|
|
892
|
-
trackSubflowStarted(subflowType, data, options, eventId) {
|
|
893
|
-
const wrappedData = {
|
|
894
|
-
subflowType,
|
|
895
|
-
...data
|
|
896
|
-
};
|
|
897
|
-
void this.track(
|
|
898
|
-
AuthEventName.SubflowStarted,
|
|
899
|
-
wrappedData,
|
|
900
|
-
options?.userReference,
|
|
901
|
-
void 0,
|
|
902
|
-
void 0,
|
|
903
|
-
options?.explicitTimestamp,
|
|
904
|
-
options?.experiments,
|
|
905
|
-
{
|
|
906
|
-
id: eventId,
|
|
907
|
-
subflowStartEventId: options?.subflowStartEventId,
|
|
908
|
-
sessionId: options?.captureSessionId,
|
|
909
|
-
initiator: options?.initiator
|
|
910
|
-
}
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
/** @internal */
|
|
914
|
-
trackSubflowTrigger(subflowType, data, options) {
|
|
915
|
-
const wrappedData = {
|
|
916
|
-
subflowType,
|
|
917
|
-
...data
|
|
918
|
-
};
|
|
919
|
-
void this.track(
|
|
920
|
-
AuthEventName.SubflowTrigger,
|
|
921
|
-
wrappedData,
|
|
922
|
-
options?.userReference,
|
|
923
|
-
void 0,
|
|
924
|
-
void 0,
|
|
925
|
-
options?.explicitTimestamp,
|
|
926
|
-
options?.experiments,
|
|
927
|
-
{
|
|
928
|
-
subflowStartEventId: options?.subflowStartEventId,
|
|
929
|
-
sessionId: options?.captureSessionId,
|
|
930
|
-
initiator: options?.initiator
|
|
931
|
-
}
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
/** @internal */
|
|
935
|
-
trackSubflowStepStarted(subflowType, stepName, data, options, ignoreAsInteraction) {
|
|
936
|
-
const wrappedData = {
|
|
937
|
-
subflowType,
|
|
938
|
-
stepName,
|
|
939
|
-
stepData: data,
|
|
940
|
-
identifier: options?.identifier,
|
|
941
|
-
ignoreAsInteraction
|
|
942
|
-
};
|
|
943
|
-
void this.track(
|
|
944
|
-
AuthEventName.SubflowStepStarted,
|
|
945
|
-
wrappedData,
|
|
946
|
-
options?.userReference,
|
|
947
|
-
void 0,
|
|
948
|
-
void 0,
|
|
949
|
-
options?.explicitTimestamp,
|
|
950
|
-
options?.experiments,
|
|
951
|
-
{
|
|
952
|
-
subflowStartEventId: options?.subflowStartEventId,
|
|
953
|
-
sessionId: options?.captureSessionId,
|
|
954
|
-
initiator: options?.initiator
|
|
955
|
-
}
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
/** @internal */
|
|
959
|
-
trackSubflowStepFinished(subflowType, stepName, data, options) {
|
|
960
|
-
const wrappedData = {
|
|
961
|
-
subflowType,
|
|
962
|
-
stepName,
|
|
963
|
-
stepData: data,
|
|
964
|
-
identifier: options?.identifier
|
|
965
|
-
};
|
|
966
|
-
void this.track(
|
|
967
|
-
AuthEventName.SubflowStepFinished,
|
|
968
|
-
wrappedData,
|
|
969
|
-
options?.userReference,
|
|
970
|
-
void 0,
|
|
971
|
-
void 0,
|
|
972
|
-
options?.explicitTimestamp,
|
|
973
|
-
options?.experiments,
|
|
974
|
-
{
|
|
975
|
-
subflowStartEventId: options?.subflowStartEventId,
|
|
976
|
-
sessionId: options?.captureSessionId,
|
|
977
|
-
initiator: options?.initiator
|
|
978
|
-
}
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
/** @internal */
|
|
982
|
-
trackSubflowStepError(subflowType, stepName, data, options) {
|
|
983
|
-
const wrappedData = {
|
|
984
|
-
subflowType,
|
|
985
|
-
stepName,
|
|
986
|
-
stepData: data,
|
|
987
|
-
identifier: options?.identifier
|
|
988
|
-
};
|
|
989
|
-
void this.track(
|
|
990
|
-
AuthEventName.SubflowStepError,
|
|
991
|
-
wrappedData,
|
|
992
|
-
options?.userReference,
|
|
993
|
-
void 0,
|
|
994
|
-
void 0,
|
|
995
|
-
options?.explicitTimestamp,
|
|
996
|
-
options?.experiments,
|
|
997
|
-
{
|
|
998
|
-
subflowStartEventId: options?.subflowStartEventId,
|
|
999
|
-
sessionId: options?.captureSessionId,
|
|
1000
|
-
initiator: options?.initiator
|
|
1001
|
-
}
|
|
1002
|
-
);
|
|
84
|
+
clearExperimentsOn(this);
|
|
1003
85
|
}
|
|
1004
86
|
/**
|
|
1005
87
|
* @remarks
|
|
@@ -1008,153 +90,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1008
90
|
* step errors are the intended channel; not part of a normal integration.
|
|
1009
91
|
*/
|
|
1010
92
|
trackSubflowError(subflowType, data, options) {
|
|
1011
|
-
|
|
1012
|
-
subflowType,
|
|
1013
|
-
stepData: data
|
|
1014
|
-
};
|
|
1015
|
-
void this.track(
|
|
1016
|
-
AuthEventName.SubflowError,
|
|
1017
|
-
wrappedData,
|
|
1018
|
-
options?.userReference,
|
|
1019
|
-
void 0,
|
|
1020
|
-
void 0,
|
|
1021
|
-
options?.explicitTimestamp,
|
|
1022
|
-
options?.experiments,
|
|
1023
|
-
{
|
|
1024
|
-
subflowStartEventId: options?.subflowStartEventId,
|
|
1025
|
-
sessionId: options?.captureSessionId,
|
|
1026
|
-
initiator: options?.initiator
|
|
1027
|
-
}
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
/**
|
|
1031
|
-
* Error situations:
|
|
1032
|
-
* - Blocked user agent (see `isBlockedUserAgent`): skip with a debug log and return
|
|
1033
|
-
* - Any error before the event is enqueued (e.g. `JSON.stringify` in debug logging, `queue.enqueue`): log `Failed to process event "<name>" before enqueue`
|
|
1034
|
-
*
|
|
1035
|
-
* @internal
|
|
1036
|
-
*/
|
|
1037
|
-
async track(name, data, userReference, tags, contexts, explicitTimestamp, experiments, capture) {
|
|
1038
|
-
try {
|
|
1039
|
-
if (this.isTrackingBlocked()) {
|
|
1040
|
-
this.logger.debug(`Skipping event "${name}" for blocked user agent.`);
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
if (name === AuthEventName.FlowFinished || name === AuthEventName.FlowAutoFinished || name === AuthEventName.FlowReset) {
|
|
1044
|
-
this.windowObserver?.flushInputs();
|
|
1045
|
-
}
|
|
1046
|
-
const timestamp = explicitTimestamp ?? Date.now();
|
|
1047
|
-
const capturedId = capture?.id ?? uuidv7();
|
|
1048
|
-
const capturedSessionId = capture?.sessionId ?? this.sessionId;
|
|
1049
|
-
const nextSeq = this.nextSeq();
|
|
1050
|
-
const seq = typeof nextSeq === "number" ? nextSeq : await nextSeq;
|
|
1051
|
-
const trackingSourcePath = this.resolveTrackingSourcePath();
|
|
1052
|
-
const event = {
|
|
1053
|
-
type: "predefined",
|
|
1054
|
-
name,
|
|
1055
|
-
id: capturedId,
|
|
1056
|
-
timestamp,
|
|
1057
|
-
seq,
|
|
1058
|
-
data,
|
|
1059
|
-
user: userReference,
|
|
1060
|
-
contexts,
|
|
1061
|
-
tags,
|
|
1062
|
-
experiments: this.resolveExperiments(experiments),
|
|
1063
|
-
meta: {
|
|
1064
|
-
captureVersion: 1,
|
|
1065
|
-
pageId: getCapturePageId(),
|
|
1066
|
-
initiator: capture?.initiator,
|
|
1067
|
-
trackingSourcePath: trackingSourcePath ?? "",
|
|
1068
|
-
subflowStartEventId: capture?.subflowStartEventId,
|
|
1069
|
-
tabId: this.resolveEventTabId()
|
|
1070
|
-
}
|
|
1071
|
-
};
|
|
1072
|
-
const deviceInfo = this.deviceInfoManager.getForEvent();
|
|
1073
|
-
if (deviceInfo) {
|
|
1074
|
-
event.deviceInfo = deviceInfo;
|
|
1075
|
-
}
|
|
1076
|
-
this.logger.debug(`Track: ${name}`, JSON.stringify(event, null, 2));
|
|
1077
|
-
const passwordManagers = this.passwordManagerSampler?.takeReport(capturedSessionId);
|
|
1078
|
-
if (passwordManagers) {
|
|
1079
|
-
event.passwordManagers = passwordManagers;
|
|
1080
|
-
}
|
|
1081
|
-
this.queue.enqueue(event, capturedSessionId);
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
this.logger.error(`Failed to process event "${name}" before enqueue`, error);
|
|
1084
|
-
this.reportTelemetry("error", `failed to process event "${name}" before enqueue`);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
// --- Typed Tracking Methods ---
|
|
1088
|
-
/**
|
|
1089
|
-
* Track when an authentication flow (or flow selection screen) becomes visible.
|
|
1090
|
-
*
|
|
1091
|
-
* @remarks
|
|
1092
|
-
* Call this once when the user first enters an auth touchpoint (for example account login,
|
|
1093
|
-
* checkout login, or account recovery). If users can choose between multiple flows
|
|
1094
|
-
* (such as login vs signup), pass `flowNames` and optionally `defaultFlowName`.
|
|
1095
|
-
*
|
|
1096
|
-
* The returned handle can attach identity after completion with `await flow.enrich({ userId })`.
|
|
1097
|
-
* Enrichment requires a backend supporting explicit event targeting.
|
|
1098
|
-
*
|
|
1099
|
-
* @param data - Flow metadata, including one flow (`flowName`) or multiple candidates (`flowNames`).
|
|
1100
|
-
* @param tags - Optional key-value tags for filtering and segmentation.
|
|
1101
|
-
* @param experiments - Optional per-call experiment overrides.
|
|
1102
|
-
* @param contexts - Optional structured event context (e.g. technical metadata that is not an
|
|
1103
|
-
* analytical dimension). Written to the event's `contexts` field verbatim.
|
|
1104
|
-
* @param options - Optional `explicitTimestamp` (epoch ms) when the flow became visible earlier than
|
|
1105
|
-
* this call, e.g. because an integration confirms a screen only after it stayed up for a while.
|
|
1106
|
-
*
|
|
1107
|
-
* @example
|
|
1108
|
-
* ```typescript
|
|
1109
|
-
* tracker.flowStarted({
|
|
1110
|
-
* flowNames: ["login", "signup"],
|
|
1111
|
-
* defaultFlowName: "login",
|
|
1112
|
-
* touchpoint: "account",
|
|
1113
|
-
* });
|
|
1114
|
-
* ```
|
|
1115
|
-
*/
|
|
1116
|
-
flowStarted(data, tags, experiments, contexts, options) {
|
|
1117
|
-
try {
|
|
1118
|
-
const targetEventId = uuidv7();
|
|
1119
|
-
const sessionId = this.sessionId;
|
|
1120
|
-
const effectiveTags = { ...this.options.defaultTags, ...this.applicationTag(), ...tags };
|
|
1121
|
-
void this.track(
|
|
1122
|
-
AuthEventName.FlowStarted,
|
|
1123
|
-
data,
|
|
1124
|
-
void 0,
|
|
1125
|
-
effectiveTags,
|
|
1126
|
-
contexts,
|
|
1127
|
-
options?.explicitTimestamp,
|
|
1128
|
-
experiments,
|
|
1129
|
-
{
|
|
1130
|
-
id: targetEventId,
|
|
1131
|
-
sessionId
|
|
1132
|
-
}
|
|
1133
|
-
);
|
|
1134
|
-
return this.flowHandle(targetEventId, sessionId);
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
this.logger.warn("Failed to start flow", error);
|
|
1137
|
-
return { enrich: async () => {
|
|
1138
|
-
} };
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
/**
|
|
1142
|
-
* Track when the user commits to a specific flow.
|
|
1143
|
-
*
|
|
1144
|
-
* @remarks
|
|
1145
|
-
* Use this after presenting alternatives and once you know which path the user takes
|
|
1146
|
-
* (for example when identifier checks route to login vs signup).
|
|
1147
|
-
*
|
|
1148
|
-
* @param data - Selected flow name.
|
|
1149
|
-
* @param tags - Optional key-value tags for filtering and segmentation.
|
|
1150
|
-
*
|
|
1151
|
-
* @example
|
|
1152
|
-
* ```typescript
|
|
1153
|
-
* tracker.flowDecided({flowName: "signup"});
|
|
1154
|
-
* ```
|
|
1155
|
-
*/
|
|
1156
|
-
flowDecided(data, tags, experiments) {
|
|
1157
|
-
void this.track(AuthEventName.FlowDecided, data, void 0, tags, void 0, void 0, experiments);
|
|
93
|
+
trackSubflowError(this, subflowType, data, options);
|
|
1158
94
|
}
|
|
1159
95
|
/**
|
|
1160
96
|
* Track a business/marketing conversion that is not part of an authentication flow.
|
|
@@ -1180,118 +116,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1180
116
|
* ```
|
|
1181
117
|
*/
|
|
1182
118
|
conversion(data, tags, experiments) {
|
|
1183
|
-
|
|
1184
|
-
const userReference = { userId, identifier, crossEnvironmentTransactionID };
|
|
1185
|
-
const effectiveTags = { ...this.options.defaultTags, ...this.applicationTag(), ...tags };
|
|
1186
|
-
void this.track(AuthEventName.Conversion, dataOut, userReference, effectiveTags, void 0, void 0, experiments);
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Track when a flow successfully completes.
|
|
1190
|
-
*
|
|
1191
|
-
* @remarks
|
|
1192
|
-
* Fire this when the user has finished the target flow end-to-end, such as completed login,
|
|
1193
|
-
* signup, or recovery. Record identity with `setUser()` while the flow is active,
|
|
1194
|
-
* or enrich it after completion through the handle returned by `flowStarted()` or by this call.
|
|
1195
|
-
*
|
|
1196
|
-
* The returned handle targets the flow this event finished, so identity learned only after
|
|
1197
|
-
* completion — for example from a profile request that follows account creation — can be
|
|
1198
|
-
* attached with `await finished.enrich({ userId })` even on a page that never saw the flow start.
|
|
1199
|
-
* Enrichment requires a backend supporting explicit flow-event targeting.
|
|
1200
|
-
*
|
|
1201
|
-
* Legacy identity fields in `data` remain supported but are deprecated.
|
|
1202
|
-
*
|
|
1203
|
-
* @param data - Finished flow name, optional explicit outcome, and deprecated user reference fields.
|
|
1204
|
-
* @param tags - Optional key-value tags for filtering and segmentation.
|
|
1205
|
-
*
|
|
1206
|
-
* @example
|
|
1207
|
-
* ```typescript
|
|
1208
|
-
* const finished = tracker.flowFinished({ flowName: "signup" });
|
|
1209
|
-
* const userId = await hashUserId(createdAccountId);
|
|
1210
|
-
* await finished.enrich({ userId });
|
|
1211
|
-
* ```
|
|
1212
|
-
*/
|
|
1213
|
-
flowFinished(data, tags, experiments) {
|
|
1214
|
-
try {
|
|
1215
|
-
const { userId, identifier, ...dataOut } = data;
|
|
1216
|
-
const userReference = { userId, identifier };
|
|
1217
|
-
const targetEventId = uuidv7();
|
|
1218
|
-
const sessionId = this.sessionId;
|
|
1219
|
-
void this.track(AuthEventName.FlowFinished, dataOut, userReference, tags, void 0, void 0, experiments, {
|
|
1220
|
-
id: targetEventId,
|
|
1221
|
-
sessionId
|
|
1222
|
-
});
|
|
1223
|
-
return this.flowHandle(targetEventId, sessionId);
|
|
1224
|
-
} catch (error) {
|
|
1225
|
-
this.logger.warn("Failed to finish flow", error);
|
|
1226
|
-
return { enrich: async () => {
|
|
1227
|
-
} };
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
/** Emits `flow_enriched` against an explicit target, in the session that emitted the target. */
|
|
1231
|
-
flowHandle(targetEventId, sessionId) {
|
|
1232
|
-
return {
|
|
1233
|
-
enrich: async (user) => {
|
|
1234
|
-
try {
|
|
1235
|
-
await this.track(
|
|
1236
|
-
AuthEventName.FlowEnriched,
|
|
1237
|
-
{ targetEventId },
|
|
1238
|
-
{ ...user },
|
|
1239
|
-
void 0,
|
|
1240
|
-
void 0,
|
|
1241
|
-
void 0,
|
|
1242
|
-
void 0,
|
|
1243
|
-
{ sessionId }
|
|
1244
|
-
);
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
this.logger.warn("Failed to enrich flow", error);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
/**
|
|
1252
|
-
* Track when an in-progress flow is reset and restarted.
|
|
1253
|
-
*
|
|
1254
|
-
* @remarks
|
|
1255
|
-
* Use this when users abandon the current path and return to the beginning of the same flow,
|
|
1256
|
-
* for example after switching account/identifier and reopening the first step.
|
|
1257
|
-
*
|
|
1258
|
-
* @param data - Flow name being reset.
|
|
1259
|
-
* @param tags - Optional key-value tags for filtering and segmentation.
|
|
1260
|
-
*
|
|
1261
|
-
* @example
|
|
1262
|
-
* ```typescript
|
|
1263
|
-
* tracker.flowReset({flowName: "login"});
|
|
1264
|
-
* ```
|
|
1265
|
-
*/
|
|
1266
|
-
flowReset(data, tags, experiments) {
|
|
1267
|
-
void this.track(AuthEventName.FlowReset, data, void 0, tags, void 0, void 0, experiments);
|
|
1268
|
-
}
|
|
1269
|
-
/**
|
|
1270
|
-
* Track when a flow is considered finished because another flow finished it implicitly.
|
|
1271
|
-
*
|
|
1272
|
-
* @remarks
|
|
1273
|
-
* Use this for automatic completions where no separate explicit finish action happens in the
|
|
1274
|
-
* current flow. Set `flowName` to the auto-finished flow and `finishedByFlowName` to the flow
|
|
1275
|
-
* that caused it. Record identity with `setUser()` while the intended flow is still
|
|
1276
|
-
* current, before it is auto-finished.
|
|
1277
|
-
*
|
|
1278
|
-
* Legacy identity fields in `data` remain supported but are deprecated.
|
|
1279
|
-
*
|
|
1280
|
-
* @param data - Auto-finished flow, finishing flow, and deprecated user reference fields.
|
|
1281
|
-
* @param tags - Optional key-value tags for filtering and segmentation.
|
|
1282
|
-
*
|
|
1283
|
-
* @example
|
|
1284
|
-
* ```typescript
|
|
1285
|
-
* tracker.flowAutoFinished({
|
|
1286
|
-
* flowName: "signup",
|
|
1287
|
-
* finishedByFlowName: "login",
|
|
1288
|
-
* });
|
|
1289
|
-
* ```
|
|
1290
|
-
*/
|
|
1291
|
-
flowAutoFinished(data, tags, experiments) {
|
|
1292
|
-
const { userId, identifier, ...dataOut } = data;
|
|
1293
|
-
const userReference = { userId, identifier };
|
|
1294
|
-
void this.track(AuthEventName.FlowAutoFinished, dataOut, userReference, tags, void 0, void 0, experiments);
|
|
119
|
+
conversion(this, data, tags, experiments);
|
|
1295
120
|
}
|
|
1296
121
|
/**
|
|
1297
122
|
* Record a newly known user reference for the current active flow.
|
|
@@ -1315,7 +140,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1315
140
|
* ```
|
|
1316
141
|
*/
|
|
1317
142
|
setUser(user) {
|
|
1318
|
-
|
|
143
|
+
setUserOn(this, user);
|
|
1319
144
|
}
|
|
1320
145
|
/**
|
|
1321
146
|
* Attach tags to the flow that is open right now.
|
|
@@ -1347,69 +172,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1347
172
|
* ```
|
|
1348
173
|
*/
|
|
1349
174
|
setFlowTags(tags, options) {
|
|
1350
|
-
|
|
1351
|
-
if (!tags || typeof tags !== "object") {
|
|
1352
|
-
this.logger.warn("setFlowTags: ignoring non-object tags");
|
|
1353
|
-
return;
|
|
1354
|
-
}
|
|
1355
|
-
const valid = {};
|
|
1356
|
-
for (const [key, value] of Object.entries(tags)) {
|
|
1357
|
-
if (key === "" || typeof value !== "string") {
|
|
1358
|
-
this.logger.warn(`setFlowTags: ignoring invalid tag "${key}"`);
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
valid[key] = value;
|
|
1362
|
-
}
|
|
1363
|
-
if (Object.keys(valid).length === 0) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
const data = { match: { flowType: options?.flowType ?? "*", at: "during" } };
|
|
1367
|
-
if (options?.expandToChildren !== void 0) {
|
|
1368
|
-
data.expCh = options.expandToChildren;
|
|
1369
|
-
}
|
|
1370
|
-
if (options?.expandToFollowing !== void 0) {
|
|
1371
|
-
data.expFol = options.expandToFollowing;
|
|
1372
|
-
}
|
|
1373
|
-
void this.track(AuthEventName.FlowEnriched, data, void 0, valid);
|
|
1374
|
-
} catch (error) {
|
|
1375
|
-
this.logger.warn("Failed to set flow tags", error);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
/**
|
|
1379
|
-
* Track when you present authentication method choices to the user.
|
|
1380
|
-
*
|
|
1381
|
-
* @remarks
|
|
1382
|
-
* Use this whenever a decision point is shown with a concrete method set, such as
|
|
1383
|
-
* identifier entry options (`d1`) or post-identifier options (`d3`). Set
|
|
1384
|
-
* `explicitDecisionValue` when the options are re-offered after a user action and you want
|
|
1385
|
-
* to capture what triggered that transition.
|
|
1386
|
-
*
|
|
1387
|
-
* @param data - Decision identifier, offered options, and optional transition reason.
|
|
1388
|
-
* @param tags - Optional key-value tags for filtering and segmentation.
|
|
1389
|
-
* @param experiments - Optional per-call experiment assignments.
|
|
1390
|
-
* @param options - Optional explicit event timestamp.
|
|
1391
|
-
*
|
|
1392
|
-
* @example
|
|
1393
|
-
* ```typescript
|
|
1394
|
-
* tracker.authMethodsDecisionStarted({
|
|
1395
|
-
* decisionName: "d3",
|
|
1396
|
-
* options: ["passkey", "social-google", "password", "reset-flow"],
|
|
1397
|
-
* });
|
|
1398
|
-
* ```
|
|
1399
|
-
*/
|
|
1400
|
-
authMethodsDecisionStarted(data, tags, experiments, options) {
|
|
1401
|
-
void this.track(
|
|
1402
|
-
AuthEventName.AuthMethodDecisionStarted,
|
|
1403
|
-
data,
|
|
1404
|
-
void 0,
|
|
1405
|
-
tags,
|
|
1406
|
-
void 0,
|
|
1407
|
-
options?.explicitTimestamp,
|
|
1408
|
-
experiments
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
authMethodsDecisionFinished(data, tags, experiments) {
|
|
1412
|
-
void this.track(AuthEventName.AuthMethodDecisionFinished, data, void 0, tags, void 0, void 0, experiments);
|
|
175
|
+
setFlowTagsOn(this, tags, options);
|
|
1413
176
|
}
|
|
1414
177
|
// decisions
|
|
1415
178
|
/**
|
|
@@ -1417,75 +180,14 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1417
180
|
* already accept free-form strings).
|
|
1418
181
|
*/
|
|
1419
182
|
authDecisionStarted(data, tags, experiments) {
|
|
1420
|
-
|
|
183
|
+
authDecisionStarted(this, data, tags, experiments);
|
|
1421
184
|
}
|
|
1422
185
|
/**
|
|
1423
186
|
* @deprecated Use {@link CorbadoTracker.authMethodsDecisionFinished} instead (its `options`
|
|
1424
187
|
* already accept free-form strings).
|
|
1425
188
|
*/
|
|
1426
189
|
authDecisionFinished(data, tags, experiments) {
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
/**
|
|
1430
|
-
* Enqueue a "low" event (granular DOM/window/visualviewport/PWM signal) for the current session.
|
|
1431
|
-
*
|
|
1432
|
-
* @remarks
|
|
1433
|
-
* Low events are sent in the `lows` array of the events endpoint payload, alongside any
|
|
1434
|
-
* pending typed events. Blocked user agents are skipped silently.
|
|
1435
|
-
*
|
|
1436
|
-
* @internal
|
|
1437
|
-
*/
|
|
1438
|
-
enqueueLowEvent(low, capturedSessionId) {
|
|
1439
|
-
try {
|
|
1440
|
-
if (this.isTrackingBlocked()) {
|
|
1441
|
-
this.logger.debug(`Skipping low event "${low.lowType}" for blocked user agent.`);
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
this.queue.enqueueLow(
|
|
1445
|
-
{
|
|
1446
|
-
...low,
|
|
1447
|
-
captureSeq: low.captureSeq ?? reserveCaptureSeq(),
|
|
1448
|
-
captureVersion: 1,
|
|
1449
|
-
pageId: low.pageId ?? getCapturePageId()
|
|
1450
|
-
},
|
|
1451
|
-
capturedSessionId
|
|
1452
|
-
);
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
this.logger.error(`Failed to process low event "${low.lowType}" before enqueue`, error);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Record a diagnostic telemetry entry (a `level` + free-form `message`) for the current session.
|
|
1459
|
-
*
|
|
1460
|
-
* @remarks
|
|
1461
|
-
* Telemetry is delivered in the `telemetry` array of the events endpoint payload, tied to the same
|
|
1462
|
-
* session, and is never classified as an auth-flow event. By default a telemetry entry piggybacks
|
|
1463
|
-
* the next normal flush (it does not trigger one on its own); this is server-controlled. Collection
|
|
1464
|
-
* can be turned off server-side via the `telemetry` config flag. Blocked user agents are skipped.
|
|
1465
|
-
*
|
|
1466
|
-
* The message is truncated to {@link TELEMETRY_MESSAGE_MAX} characters. Never pass credentials or
|
|
1467
|
-
* PII — telemetry is for diagnostics only. Like all SDK entry points, this never throws.
|
|
1468
|
-
*/
|
|
1469
|
-
telemetry(level, message) {
|
|
1470
|
-
try {
|
|
1471
|
-
if (this.isTrackingBlocked()) {
|
|
1472
|
-
this.logger.debug("Skipping telemetry for blocked user agent.");
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
const text = typeof message === "string" ? message : String(message);
|
|
1476
|
-
if (text.length === 0) {
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
const entry = {
|
|
1480
|
-
id: uuidv7(),
|
|
1481
|
-
level,
|
|
1482
|
-
message: text.length > TELEMETRY_MESSAGE_MAX ? text.slice(0, TELEMETRY_MESSAGE_MAX) : text,
|
|
1483
|
-
ts: Date.now()
|
|
1484
|
-
};
|
|
1485
|
-
this.queue.enqueueTelemetry(entry);
|
|
1486
|
-
} catch (error) {
|
|
1487
|
-
this.logger.error("Failed to process telemetry before enqueue", error);
|
|
1488
|
-
}
|
|
190
|
+
authDecisionFinished(this, data, tags, experiments);
|
|
1489
191
|
}
|
|
1490
192
|
/**
|
|
1491
193
|
* Queue a probe item (diagnostic capture produced by a Corbado autocapture bundle).
|
|
@@ -1500,53 +202,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1500
202
|
* @internal
|
|
1501
203
|
*/
|
|
1502
204
|
probe(input) {
|
|
1503
|
-
|
|
1504
|
-
if (this.isTrackingBlocked()) {
|
|
1505
|
-
this.logger.debug("Skipping probe for blocked user agent.");
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
if (!input || typeof input !== "object" || typeof input.kind !== "string" || !PROBE_KIND_RE.test(input.kind)) {
|
|
1509
|
-
this.logger.debug("Dropping probe with an invalid kind.");
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
const payload = input.payload;
|
|
1513
|
-
if (!payload || typeof payload !== "object") {
|
|
1514
|
-
this.logger.debug(`Dropping probe "${input.kind}" without an object payload.`);
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
const pageId = typeof input.pageId === "string" && PROBE_ID_RE.test(input.pageId) ? input.pageId : this.defaultProbePageId();
|
|
1518
|
-
const entry = {
|
|
1519
|
-
id: uuidv7(),
|
|
1520
|
-
kind: input.kind,
|
|
1521
|
-
ts: typeof input.ts === "number" && Number.isFinite(input.ts) ? Math.round(input.ts) : Date.now(),
|
|
1522
|
-
pageId,
|
|
1523
|
-
seq: this.nextProbeSeq(pageId),
|
|
1524
|
-
payload
|
|
1525
|
-
};
|
|
1526
|
-
if (typeof input.cfg === "string" && PROBE_ID_RE.test(input.cfg)) {
|
|
1527
|
-
entry.cfg = input.cfg;
|
|
1528
|
-
}
|
|
1529
|
-
const ctx = sanitizeProbeContext(input.ctx);
|
|
1530
|
-
if (ctx) {
|
|
1531
|
-
entry.ctx = ctx;
|
|
1532
|
-
}
|
|
1533
|
-
this.queue.enqueueProbe(entry);
|
|
1534
|
-
} catch (error) {
|
|
1535
|
-
this.logger.error("Failed to process probe before enqueue", error);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
defaultProbePageId() {
|
|
1539
|
-
return this.probePageId ??= randomProbePageId();
|
|
1540
|
-
}
|
|
1541
|
-
nextProbeSeq(pageId) {
|
|
1542
|
-
const seq = this.probeSeqs.get(pageId) ?? 0;
|
|
1543
|
-
this.probeSeqs.delete(pageId);
|
|
1544
|
-
this.probeSeqs.set(pageId, seq + 1);
|
|
1545
|
-
if (this.probeSeqs.size > PROBE_PAGE_IDS_MAX) {
|
|
1546
|
-
const oldest = this.probeSeqs.keys().next().value;
|
|
1547
|
-
if (oldest !== void 0) this.probeSeqs.delete(oldest);
|
|
1548
|
-
}
|
|
1549
|
-
return seq;
|
|
205
|
+
probe(this, input);
|
|
1550
206
|
}
|
|
1551
207
|
/** Convenience wrapper for {@link CorbadoTracker.telemetry} at `info` level. */
|
|
1552
208
|
logInfo(message) {
|
|
@@ -1556,91 +212,6 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1556
212
|
logError(message) {
|
|
1557
213
|
this.telemetry("error", message);
|
|
1558
214
|
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Queue hook run right before an event batch is sent: a device info snapshot that no event carried
|
|
1561
|
-
* yet (typically the fast snapshot after a burst of events at page load, or the privateMode update)
|
|
1562
|
-
* rides the first event of this page in the batch that has none. Events of other pages (outbox
|
|
1563
|
-
* replays) are never touched. A snapshot no event can carry goes as the batch's request-level device
|
|
1564
|
-
* info instead (queue option `standaloneDeviceInfo`).
|
|
1565
|
-
*
|
|
1566
|
-
* Error situations:
|
|
1567
|
-
* - Any error: log "Failed to attach device info to batch" and send the batch unchanged
|
|
1568
|
-
*/
|
|
1569
|
-
attachUndeliveredDeviceInfo(events) {
|
|
1570
|
-
try {
|
|
1571
|
-
const manager = this.deviceInfoManager;
|
|
1572
|
-
if (!manager?.hasUndelivered()) {
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
const pageId = getCapturePageId();
|
|
1576
|
-
const target = events.find((event) => event.meta?.pageId === pageId && !event.deviceInfo);
|
|
1577
|
-
if (target) {
|
|
1578
|
-
target.deviceInfo = manager.takeUndelivered();
|
|
1579
|
-
}
|
|
1580
|
-
} catch (error) {
|
|
1581
|
-
this.logger.error("Failed to attach device info to batch", error);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* Escalate an internal SDK diagnostic to the telemetry stream (self-instrumentation). This is the
|
|
1586
|
-
* sink behind {@link Logger.reportTelemetry}; chosen internal failure sites route here so faults
|
|
1587
|
-
* that are otherwise only visible with `debug: true` become observable in the field.
|
|
1588
|
-
*
|
|
1589
|
-
* @remarks
|
|
1590
|
-
* Bounded and deduped per load (a persistent fault — blocked storage, a downed endpoint — cannot
|
|
1591
|
-
* flood the stream), re-entrancy-guarded so it can never recurse, and it never throws into the
|
|
1592
|
-
* host. Diagnostics raised before the queue exists (early boot) are buffered and replayed once the
|
|
1593
|
-
* queue is ready. Messages must be stable and low-cardinality (no raw error objects / PII).
|
|
1594
|
-
*/
|
|
1595
|
-
reportTelemetry(level, message) {
|
|
1596
|
-
if (this.telemetryReporting) {
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
this.telemetryReporting = true;
|
|
1600
|
-
try {
|
|
1601
|
-
if (this.reportedTelemetryKeys.has(message) || this.reportedTelemetryKeys.size >= MAX_SELF_TELEMETRY) {
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
this.reportedTelemetryKeys.add(message);
|
|
1605
|
-
if (!this.telemetryReady) {
|
|
1606
|
-
this.pendingSelfTelemetry.push({ level, message });
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
this.telemetry(level, message);
|
|
1610
|
-
} catch {
|
|
1611
|
-
} finally {
|
|
1612
|
-
this.telemetryReporting = false;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
/**
|
|
1616
|
-
* Synchronously flush any pending events and low events via beacon. Use during page-unload
|
|
1617
|
-
* style teardown where async flushing is unreliable.
|
|
1618
|
-
*
|
|
1619
|
-
* @param reason What triggered the flush; recorded as `meta.flushReason` on the batch. Defaults
|
|
1620
|
-
* to a direct call by the host app.
|
|
1621
|
-
*
|
|
1622
|
-
* @internal
|
|
1623
|
-
*/
|
|
1624
|
-
flushKeepalive(reason = "manual") {
|
|
1625
|
-
try {
|
|
1626
|
-
this.queue.flushKeepalive(reason);
|
|
1627
|
-
} catch (error) {
|
|
1628
|
-
this.logger.error("Failed to flush queue keepalive", error);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
/**
|
|
1632
|
-
* Keepalive flush requested by a low-event tracker's `pagehide` teardown. Gated by the
|
|
1633
|
-
* server-controlled trigger switch `tlf` (boot-snapshot, default true = historical behavior):
|
|
1634
|
-
* disabling it server-side makes low-event delivery rely solely on the queue's own triggers.
|
|
1635
|
-
*
|
|
1636
|
-
* @internal
|
|
1637
|
-
*/
|
|
1638
|
-
lowEventFlushKeepalive() {
|
|
1639
|
-
if (!this.config.tlf) {
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
this.flushKeepalive("low-event-teardown");
|
|
1643
|
-
}
|
|
1644
215
|
passkeyLoginFullOperation(config) {
|
|
1645
216
|
return new PasskeyLoginOperationFull(this, config);
|
|
1646
217
|
}
|
|
@@ -1873,73 +444,6 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1873
444
|
trustedDeviceEnrollmentOperationFull(config) {
|
|
1874
445
|
return new TrustedDeviceEnrollmentOperationFull(this, config);
|
|
1875
446
|
}
|
|
1876
|
-
/**
|
|
1877
|
-
* Flush any pending events and release all resources (timers, event listeners).
|
|
1878
|
-
*
|
|
1879
|
-
* @remarks
|
|
1880
|
-
* After calling destroy, the tracker must not be used. Operation objects
|
|
1881
|
-
* returned by factory methods (e.g. `passwordLoginFullOperation`) that hold
|
|
1882
|
-
* DOM references must be destroyed separately by the caller.
|
|
1883
|
-
* The final flush is gated by the server-controlled trigger switch `td` (default true);
|
|
1884
|
-
* resources are released either way.
|
|
1885
|
-
*/
|
|
1886
|
-
async destroy() {
|
|
1887
|
-
this.crossOrigin?.stop();
|
|
1888
|
-
this.configManager?.destroy();
|
|
1889
|
-
this.passwordManagerSampler?.destroy();
|
|
1890
|
-
this.windowObserver?.destroy();
|
|
1891
|
-
this.deviceInfoManager.destroy();
|
|
1892
|
-
if (this.config.td) {
|
|
1893
|
-
try {
|
|
1894
|
-
await this.queue.flush("destroy");
|
|
1895
|
-
} catch (error) {
|
|
1896
|
-
this.logger.error("Failed to flush queue during destroy", error);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
this.queue.destroy();
|
|
1900
|
-
}
|
|
1901
|
-
// Session Management
|
|
1902
|
-
/**
|
|
1903
|
-
* Return the Observe session id this tracker is currently reporting under.
|
|
1904
|
-
*
|
|
1905
|
-
* @remarks
|
|
1906
|
-
* Use this to route a server-side event into the same session the browser reports to: read the id,
|
|
1907
|
-
* pass it to your own backend, and let the backend use it as the `sessionID` of its ingest call.
|
|
1908
|
-
* Reading it here is the supported path — the storage layout behind it is an implementation detail
|
|
1909
|
-
* that has already changed once (per-tab `sessionStorage` to the cross-tab continuity record).
|
|
1910
|
-
*
|
|
1911
|
-
* The value is **not stable for the lifetime of the page**. It changes on
|
|
1912
|
-
* {@link CorbadoTracker.resetSession}, when this tab adopts a continuity session another tab
|
|
1913
|
-
* rotated to, and when a failed bootstrap recovers. Read it at the moment you need it; never cache
|
|
1914
|
-
* it across a request.
|
|
1915
|
-
*
|
|
1916
|
-
* `undefined` means no session id could be established at all — an environment without Web Crypto,
|
|
1917
|
-
* where tracking is not working either. Skip the server-side event instead of sending an empty id:
|
|
1918
|
-
* the ingest API requires a UUID and would reject the batch.
|
|
1919
|
-
*
|
|
1920
|
-
* @returns The current session id, or `undefined` if none could be established.
|
|
1921
|
-
*
|
|
1922
|
-
* @example
|
|
1923
|
-
* ```typescript
|
|
1924
|
-
* const sessionId = tracker.getSessionId();
|
|
1925
|
-
* if (sessionId) {
|
|
1926
|
-
* await fetch("/api/checkout", {
|
|
1927
|
-
* method: "POST",
|
|
1928
|
-
* body: JSON.stringify({ ...payload, observeSessionId: sessionId }),
|
|
1929
|
-
* });
|
|
1930
|
-
* }
|
|
1931
|
-
* ```
|
|
1932
|
-
*/
|
|
1933
|
-
getSessionId() {
|
|
1934
|
-
if (!this.sessionId) {
|
|
1935
|
-
const recovered = this.resolveSession();
|
|
1936
|
-
if (recovered) {
|
|
1937
|
-
this.sessionId = recovered;
|
|
1938
|
-
this.queue.setSessionId(recovered);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
return this.sessionId || void 0;
|
|
1942
|
-
}
|
|
1943
447
|
/**
|
|
1944
448
|
* Store an opaque context string with the cross-origin session, for the integration's own use on the
|
|
1945
449
|
* next origin (e.g. a journey id and stage). Requires `fixedDataConfig.crossOriginSession`.
|
|
@@ -1964,31 +468,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1964
468
|
* ```
|
|
1965
469
|
*/
|
|
1966
470
|
setCrossOriginContext(context) {
|
|
1967
|
-
|
|
1968
|
-
if (!this.crossOrigin) {
|
|
1969
|
-
return false;
|
|
1970
|
-
}
|
|
1971
|
-
const value = context === null || context === void 0 || context === "" ? void 0 : context;
|
|
1972
|
-
if (value !== void 0 && !isValidCrossOriginContext(value)) {
|
|
1973
|
-
this.logger.debug("Ignoring invalid cross-origin context (expected <= 128 chars of [A-Za-z0-9._~-])");
|
|
1974
|
-
return false;
|
|
1975
|
-
}
|
|
1976
|
-
const seq = this.config.sessionContinuity ? Math.max(this.seq, this.persistentStorage.getItem(SEQ_STORAGE_KEY) ?? 0) : this.seq;
|
|
1977
|
-
const next = this.crossOrigin.setContext(this.sessionId, seq, value);
|
|
1978
|
-
if (next === void 0) {
|
|
1979
|
-
return false;
|
|
1980
|
-
}
|
|
1981
|
-
if (next > this.seq) {
|
|
1982
|
-
this.seq = next;
|
|
1983
|
-
if (this.config.sessionContinuity) {
|
|
1984
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, next);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
return true;
|
|
1988
|
-
} catch (error) {
|
|
1989
|
-
this.logger.error("Failed to set the cross-origin context", error);
|
|
1990
|
-
return false;
|
|
1991
|
-
}
|
|
471
|
+
return setCrossOriginContext(this, context);
|
|
1992
472
|
}
|
|
1993
473
|
/**
|
|
1994
474
|
* The context string stored with the current cross-origin session (see
|
|
@@ -1996,51 +476,13 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1996
476
|
* context, or the feature is off on this page. Never throws.
|
|
1997
477
|
*/
|
|
1998
478
|
getCrossOriginContext() {
|
|
1999
|
-
|
|
2000
|
-
return this.crossOrigin?.getContext(this.sessionId);
|
|
2001
|
-
} catch (error) {
|
|
2002
|
-
this.logger.error("Failed to read the cross-origin context", error);
|
|
2003
|
-
return void 0;
|
|
2004
|
-
}
|
|
479
|
+
return getCrossOriginContext(this);
|
|
2005
480
|
}
|
|
2006
481
|
/** @internal */
|
|
2007
482
|
resetSession() {
|
|
2008
|
-
|
|
2009
|
-
if (this.config.sessionContinuity) {
|
|
2010
|
-
this.persistContinuitySession(newSessionId, Date.now());
|
|
2011
|
-
this.persistentStorage.setItem(SEQ_STORAGE_KEY, 0);
|
|
2012
|
-
} else {
|
|
2013
|
-
this.persistPerTabSession(newSessionId, Date.now());
|
|
2014
|
-
}
|
|
2015
|
-
this.sessionId = newSessionId;
|
|
2016
|
-
this.seq = 0;
|
|
2017
|
-
this.queue.setSessionId(newSessionId);
|
|
2018
|
-
this.crossOrigin?.reset(newSessionId);
|
|
2019
|
-
this.logger.debug(`Session reset: ${newSessionId}`);
|
|
2020
|
-
return newSessionId;
|
|
483
|
+
return resetSessionOn(this);
|
|
2021
484
|
}
|
|
2022
485
|
};
|
|
2023
|
-
var PROBE_PAGE_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
2024
|
-
function randomProbePageId() {
|
|
2025
|
-
const bytes = new Uint8Array(16);
|
|
2026
|
-
try {
|
|
2027
|
-
crypto.getRandomValues(bytes);
|
|
2028
|
-
} catch {
|
|
2029
|
-
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
2030
|
-
}
|
|
2031
|
-
let out = "";
|
|
2032
|
-
for (let i = 0; i < bytes.length; i++) out += PROBE_PAGE_ID_ALPHABET[bytes[i] & 63];
|
|
2033
|
-
return out;
|
|
2034
|
-
}
|
|
2035
|
-
function sanitizeProbeContext(ctx) {
|
|
2036
|
-
if (!ctx || typeof ctx !== "object") return void 0;
|
|
2037
|
-
const out = {};
|
|
2038
|
-
for (const key of ["state", "flowType", "step"]) {
|
|
2039
|
-
const value = ctx[key];
|
|
2040
|
-
if (typeof value === "string" && value.length > 0) out[key] = value.slice(0, PROBE_CTX_MAX);
|
|
2041
|
-
}
|
|
2042
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
2043
|
-
}
|
|
2044
486
|
export {
|
|
2045
487
|
CorbadoTracker
|
|
2046
488
|
};
|