@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/default-tracker.ts
|
|
2
|
+
var current;
|
|
3
|
+
function setDefaultTracker(tracker) {
|
|
4
|
+
current = tracker;
|
|
5
|
+
}
|
|
6
|
+
function getDefaultTracker() {
|
|
7
|
+
return current;
|
|
8
|
+
}
|
|
9
|
+
function isTracker(value) {
|
|
10
|
+
return !!value && typeof value === "object" && typeof value.track === "function" && typeof value.enqueueLowEvent === "function";
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
getDefaultTracker,
|
|
14
|
+
isTracker,
|
|
15
|
+
setDefaultTracker
|
|
16
|
+
};
|
package/dist/analysis/index.mjs
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { CorbadoTracker } from "./client.mjs";
|
|
3
|
+
import { CorbadoTrackerCore } from "./tracker-core.mjs";
|
|
4
|
+
import { getDefaultTracker, setDefaultTracker } from "./default-tracker.mjs";
|
|
3
5
|
import { SDK_NAME, SDK_VERSION } from "./version.mjs";
|
|
4
6
|
export * from "./types.mjs";
|
|
5
7
|
export * from "./client.mjs";
|
|
8
|
+
export * from "./tracker-core.mjs";
|
|
9
|
+
export * from "./tracker-functions.mjs";
|
|
10
|
+
export * from "./plugins/cookie-storage.mjs";
|
|
11
|
+
export * from "./plugins/cross-origin-session.mjs";
|
|
12
|
+
export * from "./plugins/raw-errors.mjs";
|
|
13
|
+
export * from "./plugins/probes.mjs";
|
|
14
|
+
export * from "./probe-lane.mjs";
|
|
6
15
|
export * from "./transport.mjs";
|
|
7
16
|
export * from "./queue.mjs";
|
|
8
17
|
export * from "./config.mjs";
|
|
@@ -41,78 +50,57 @@ import {
|
|
|
41
50
|
sanitizeRequestOptions,
|
|
42
51
|
sanitizeAssertionResponse
|
|
43
52
|
} from "./utils/webauthn-payloads.mjs";
|
|
44
|
-
var currentTracker;
|
|
45
53
|
function init(options) {
|
|
46
54
|
const tracker = new CorbadoTracker(options);
|
|
47
|
-
|
|
55
|
+
setDefaultTracker(tracker);
|
|
56
|
+
return tracker;
|
|
57
|
+
}
|
|
58
|
+
function initCore(options) {
|
|
59
|
+
const tracker = new CorbadoTrackerCore(options);
|
|
60
|
+
setDefaultTracker(tracker);
|
|
48
61
|
return tracker;
|
|
49
62
|
}
|
|
50
63
|
function getTracker() {
|
|
51
|
-
|
|
64
|
+
const tracker = getDefaultTracker();
|
|
65
|
+
return tracker?.isFullTracker() ? tracker : void 0;
|
|
52
66
|
}
|
|
53
|
-
function
|
|
54
|
-
return
|
|
67
|
+
function getTrackerCore() {
|
|
68
|
+
return getDefaultTracker();
|
|
55
69
|
}
|
|
56
70
|
function getSessionId() {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
function setUser(user) {
|
|
60
|
-
currentTracker?.setUser(user);
|
|
61
|
-
}
|
|
62
|
-
function setFlowTags(tags, options) {
|
|
63
|
-
currentTracker?.setFlowTags(tags, options);
|
|
64
|
-
}
|
|
65
|
-
function setExperiment(key, variant) {
|
|
66
|
-
currentTracker?.setExperiment(key, variant);
|
|
71
|
+
return getDefaultTracker()?.getSessionId();
|
|
67
72
|
}
|
|
68
73
|
function setExperiments(assignments) {
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
function clearExperiment(key) {
|
|
72
|
-
currentTracker?.clearExperiment(key);
|
|
73
|
-
}
|
|
74
|
-
function clearExperiments() {
|
|
75
|
-
currentTracker?.clearExperiments();
|
|
74
|
+
getDefaultTracker()?.setExperiments(assignments);
|
|
76
75
|
}
|
|
77
76
|
function getExperiments() {
|
|
78
|
-
return
|
|
77
|
+
return getDefaultTracker()?.getExperiments() ?? {};
|
|
79
78
|
}
|
|
80
79
|
function telemetry(level, message) {
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
function logInfo(message) {
|
|
84
|
-
currentTracker?.logInfo(message);
|
|
85
|
-
}
|
|
86
|
-
function logError(message) {
|
|
87
|
-
currentTracker?.logError(message);
|
|
80
|
+
getDefaultTracker()?.telemetry(level, message);
|
|
88
81
|
}
|
|
89
82
|
async function destroy() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
const tracker = getDefaultTracker();
|
|
84
|
+
if (!tracker) return;
|
|
85
|
+
await tracker.destroy();
|
|
86
|
+
setDefaultTracker(void 0);
|
|
93
87
|
}
|
|
94
88
|
export {
|
|
95
89
|
READER_EXPOSURES,
|
|
96
90
|
SDK_NAME,
|
|
97
91
|
SDK_VERSION,
|
|
98
|
-
clearExperiment,
|
|
99
|
-
clearExperiments,
|
|
100
92
|
destroy,
|
|
101
93
|
detectPasswordManagerExtensions,
|
|
102
94
|
getExperiments,
|
|
103
95
|
getSessionId,
|
|
104
96
|
getTracker,
|
|
97
|
+
getTrackerCore,
|
|
105
98
|
init,
|
|
106
|
-
|
|
107
|
-
logInfo,
|
|
108
|
-
resetSession,
|
|
99
|
+
initCore,
|
|
109
100
|
sanitizeAssertionResponse,
|
|
110
101
|
sanitizeCreationOptions,
|
|
111
102
|
sanitizeCreationResponse,
|
|
112
103
|
sanitizeRequestOptions,
|
|
113
|
-
setExperiment,
|
|
114
104
|
setExperiments,
|
|
115
|
-
setFlowTags,
|
|
116
|
-
setUser,
|
|
117
105
|
telemetry
|
|
118
106
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/plugins/cookie-storage.ts
|
|
2
|
+
import { CookieStorage } from "../storage/index.mjs";
|
|
3
|
+
function cookieStorage(options = {}) {
|
|
4
|
+
const cookieDomain = options?.cookieDomain;
|
|
5
|
+
return {
|
|
6
|
+
storage: (logger) => ({ engine: new CookieStorage(logger, cookieDomain), source: "cookie" })
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
cookieStorage
|
|
11
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/plugins/cross-origin-session.ts
|
|
2
|
+
import { CrossOriginSession, isValidCrossOriginContext } from "../cross-origin-session.mjs";
|
|
3
|
+
import {
|
|
4
|
+
SEQ_STORAGE_KEY,
|
|
5
|
+
SESSION_CONTINUITY_KEY,
|
|
6
|
+
SESSION_STORAGE_KEY
|
|
7
|
+
} from "../session-keys.mjs";
|
|
8
|
+
function crossOriginSession(config) {
|
|
9
|
+
const cookieDomain = config && typeof config === "object" ? config.cookieDomain : void 0;
|
|
10
|
+
return {
|
|
11
|
+
session: (tracker) => typeof cookieDomain === "string" ? CrossOriginBridge.create(tracker, cookieDomain) : void 0
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var CrossOriginBridge = class _CrossOriginBridge {
|
|
15
|
+
constructor(tracker, cookie) {
|
|
16
|
+
this.tracker = tracker;
|
|
17
|
+
this.cookie = cookie;
|
|
18
|
+
}
|
|
19
|
+
/** The session this load took from the cross-origin cookie, if any (`meta.crossOriginSession`). */
|
|
20
|
+
adoptedId;
|
|
21
|
+
/** The bridge when the cookie is usable here; undefined (feature off) otherwise. Never throws. */
|
|
22
|
+
static create(tracker, cookieDomain) {
|
|
23
|
+
try {
|
|
24
|
+
if (tracker["isTrackingBlocked"]()) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
const cookie = CrossOriginSession.create({
|
|
28
|
+
projectId: tracker["options"].projectId,
|
|
29
|
+
cookieDomain,
|
|
30
|
+
logger: tracker.getLogger(),
|
|
31
|
+
inactivityMs: () => tracker["config"].sessionInactivityMs,
|
|
32
|
+
report: (level, message) => tracker["reportTelemetry"](level, message)
|
|
33
|
+
});
|
|
34
|
+
return cookie ? new _CrossOriginBridge(tracker, cookie) : void 0;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
tracker.getLogger().error("Failed to set up the cross-origin session", error);
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
resolve() {
|
|
41
|
+
const tracker = this.tracker;
|
|
42
|
+
const localActivity = this.readLocalSessionActivity();
|
|
43
|
+
tracker["sessionId"] = tracker["resolveSessionId"]();
|
|
44
|
+
this.boot(localActivity);
|
|
45
|
+
return tracker["sessionId"];
|
|
46
|
+
}
|
|
47
|
+
claim(sessionId, seq, now) {
|
|
48
|
+
return this.cookie.claim(sessionId, seq, now);
|
|
49
|
+
}
|
|
50
|
+
/** `meta.crossOriginSession` of a batch of `sessionId`; undefined while the cookie is off on this page. */
|
|
51
|
+
meta(sessionId) {
|
|
52
|
+
if (!this.cookie.isEnabled()) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
return sessionId && sessionId === this.adoptedId ? "adopted" : "own";
|
|
56
|
+
}
|
|
57
|
+
reset(sessionId) {
|
|
58
|
+
this.cookie.reset(sessionId);
|
|
59
|
+
}
|
|
60
|
+
stop() {
|
|
61
|
+
this.cookie.stop();
|
|
62
|
+
}
|
|
63
|
+
/** The seq the tracker continues from (persisted counter under continuity). */
|
|
64
|
+
localSeq() {
|
|
65
|
+
const tracker = this.tracker;
|
|
66
|
+
return tracker["config"].sessionContinuity ? Math.max(tracker["seq"], tracker["persistentStorage"].getItem(SEQ_STORAGE_KEY) ?? 0) : tracker["seq"];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Last activity of the local session records BEFORE this load rewrites them (both paths, by id), so the
|
|
70
|
+
* cross-origin boot can tell a session that was active here from one this load just created.
|
|
71
|
+
*/
|
|
72
|
+
readLocalSessionActivity() {
|
|
73
|
+
const tracker = this.tracker;
|
|
74
|
+
const activity = /* @__PURE__ */ new Map();
|
|
75
|
+
try {
|
|
76
|
+
const continuity = tracker["persistentStorage"].getItem(SESSION_CONTINUITY_KEY);
|
|
77
|
+
if (continuity?.id && typeof continuity.lastActiveAt === "number") {
|
|
78
|
+
activity.set(continuity.id, continuity.lastActiveAt);
|
|
79
|
+
}
|
|
80
|
+
const perTab = tracker["sessionStorage"].getItem(SESSION_STORAGE_KEY);
|
|
81
|
+
if (perTab && typeof perTab === "object" && perTab.id && typeof perTab.lastActiveAt === "number") {
|
|
82
|
+
activity.set(perTab.id, Math.max(activity.get(perTab.id) ?? 0, perTab.lastActiveAt));
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
tracker.getLogger().error("Failed to read local session activity", error);
|
|
86
|
+
}
|
|
87
|
+
return activity;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reconcile the session this load resolved locally with the cross-origin cookie before the first event:
|
|
91
|
+
* adopt the cookie's session (and persist it locally, so reloads and same-origin tabs follow) or keep
|
|
92
|
+
* the local one, and continue the seq at or above the cookie's mark. Never throws.
|
|
93
|
+
*/
|
|
94
|
+
boot(localActivity) {
|
|
95
|
+
const tracker = this.tracker;
|
|
96
|
+
try {
|
|
97
|
+
this.adoptedId = void 0;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const continuity = tracker["config"].sessionContinuity;
|
|
100
|
+
const sessionId = tracker["sessionId"];
|
|
101
|
+
const result = this.cookie.boot(
|
|
102
|
+
{
|
|
103
|
+
sessionId,
|
|
104
|
+
lastActiveAt: sessionId && localActivity.get(sessionId) || 0,
|
|
105
|
+
seq: this.localSeq()
|
|
106
|
+
},
|
|
107
|
+
now
|
|
108
|
+
);
|
|
109
|
+
if (!result || !result.sessionId) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (result.adopted) {
|
|
113
|
+
tracker["sessionId"] = result.sessionId;
|
|
114
|
+
this.adoptedId = result.sessionId;
|
|
115
|
+
if (continuity) {
|
|
116
|
+
tracker["persistContinuitySession"](result.sessionId, now);
|
|
117
|
+
} else {
|
|
118
|
+
tracker["persistPerTabSession"](result.sessionId, now);
|
|
119
|
+
}
|
|
120
|
+
tracker.getLogger().debug("Adopted cross-origin session id", result.sessionId);
|
|
121
|
+
}
|
|
122
|
+
tracker["seq"] = result.seq;
|
|
123
|
+
if (continuity) {
|
|
124
|
+
tracker["persistentStorage"].setItem(SEQ_STORAGE_KEY, result.seq);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
tracker.getLogger().error("Failed to apply the cross-origin session", error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function setCrossOriginContext(tracker, context) {
|
|
132
|
+
try {
|
|
133
|
+
const bridge = tracker["bridge"];
|
|
134
|
+
if (!(bridge instanceof CrossOriginBridge)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const value = context === null || context === void 0 || context === "" ? void 0 : context;
|
|
138
|
+
if (value !== void 0 && !isValidCrossOriginContext(value)) {
|
|
139
|
+
tracker.getLogger().debug("Ignoring invalid cross-origin context (expected <= 128 chars of [A-Za-z0-9._~-])");
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const next = bridge.cookie.setContext(tracker["sessionId"], bridge.localSeq(), value);
|
|
143
|
+
if (next === void 0) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (next > tracker["seq"]) {
|
|
147
|
+
tracker["seq"] = next;
|
|
148
|
+
if (tracker["config"].sessionContinuity) {
|
|
149
|
+
tracker["persistentStorage"].setItem(SEQ_STORAGE_KEY, next);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
tracker.getLogger().error("Failed to set the cross-origin context", error);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function getCrossOriginContext(tracker) {
|
|
159
|
+
try {
|
|
160
|
+
const bridge = tracker["bridge"];
|
|
161
|
+
return bridge instanceof CrossOriginBridge ? bridge.cookie.getContext(tracker["sessionId"]) : void 0;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
tracker.getLogger().error("Failed to read the cross-origin context", error);
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
crossOriginSession,
|
|
169
|
+
getCrossOriginContext,
|
|
170
|
+
setCrossOriginContext
|
|
171
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/plugins/probes.ts
|
|
2
|
+
import { v7 as uuidv7 } from "uuid";
|
|
3
|
+
import { installProbeLane } from "../probe-lane.mjs";
|
|
4
|
+
var PROBE_KIND_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5
|
+
var PROBE_ID_RE = /^[A-Za-z0-9_-]{1,16}$/;
|
|
6
|
+
var PROBE_CTX_MAX = 64;
|
|
7
|
+
function probes() {
|
|
8
|
+
return {
|
|
9
|
+
setup: (tracker) => {
|
|
10
|
+
installProbeLane(tracker["queue"]);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function probe(tracker, input) {
|
|
15
|
+
const logger = tracker.getLogger();
|
|
16
|
+
try {
|
|
17
|
+
if (tracker["isTrackingBlocked"]()) {
|
|
18
|
+
logger.debug("Skipping probe for blocked user agent.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const queue = tracker["queue"];
|
|
22
|
+
const lane = queue["lane"];
|
|
23
|
+
if (!lane) {
|
|
24
|
+
logger.debug("Dropping probe: no probes() plugin.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!input || typeof input !== "object" || typeof input.kind !== "string" || !PROBE_KIND_RE.test(input.kind)) {
|
|
28
|
+
logger.debug("Dropping probe with an invalid kind.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const payload = input.payload;
|
|
32
|
+
if (!payload || typeof payload !== "object") {
|
|
33
|
+
logger.debug(`Dropping probe "${input.kind}" without an object payload.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const pageId = typeof input.pageId === "string" && PROBE_ID_RE.test(input.pageId) ? input.pageId : lane.defaultPageId();
|
|
37
|
+
const entry = {
|
|
38
|
+
id: uuidv7(),
|
|
39
|
+
kind: input.kind,
|
|
40
|
+
ts: typeof input.ts === "number" && Number.isFinite(input.ts) ? Math.round(input.ts) : Date.now(),
|
|
41
|
+
pageId,
|
|
42
|
+
seq: lane.nextSeq(pageId),
|
|
43
|
+
payload
|
|
44
|
+
};
|
|
45
|
+
if (typeof input.cfg === "string" && PROBE_ID_RE.test(input.cfg)) {
|
|
46
|
+
entry.cfg = input.cfg;
|
|
47
|
+
}
|
|
48
|
+
const ctx = sanitizeProbeContext(input.ctx);
|
|
49
|
+
if (ctx) {
|
|
50
|
+
entry.ctx = ctx;
|
|
51
|
+
}
|
|
52
|
+
queue.enqueueProbe(entry);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.error("Failed to process probe before enqueue", error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function sanitizeProbeContext(ctx) {
|
|
58
|
+
if (!ctx || typeof ctx !== "object") return void 0;
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const key of ["state", "flowType", "step"]) {
|
|
61
|
+
const value = ctx[key];
|
|
62
|
+
if (typeof value === "string" && value.length > 0) out[key] = value.slice(0, PROBE_CTX_MAX);
|
|
63
|
+
}
|
|
64
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
probe,
|
|
68
|
+
probes
|
|
69
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/probe-lane.ts
|
|
2
|
+
import { utf8Length } from "./queue.mjs";
|
|
3
|
+
var PROBE_QUEUE_MAX_ITEMS = 500;
|
|
4
|
+
var PROBE_QUEUE_MAX_BYTES = 512 * 1024;
|
|
5
|
+
var PROBE_BATCH_MAX_ITEMS = 200;
|
|
6
|
+
var PROBE_BATCH_MAX_BYTES = 200 * 1024;
|
|
7
|
+
var PROBE_PAYLOAD_MAX_BYTES = 16 * 1024;
|
|
8
|
+
var UNLOAD_PROBE_MAX_BYTES = 32e3;
|
|
9
|
+
function installProbeLane(queue) {
|
|
10
|
+
const existing = queue["lane"];
|
|
11
|
+
if (existing) return existing;
|
|
12
|
+
const lane = new ProbeLane(queue);
|
|
13
|
+
queue["lane"] = lane;
|
|
14
|
+
queue.enqueueProbe = (entry, capturedSessionId = queue["sessionId"]) => {
|
|
15
|
+
try {
|
|
16
|
+
if (!queue["destroyed"]) lane.enqueue(entry, capturedSessionId);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
queue["logger"].error("Failed to enqueue probe", error);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
queue.droppedProbeCount = () => lane.dropped;
|
|
22
|
+
return lane;
|
|
23
|
+
}
|
|
24
|
+
var PROBE_PAGE_IDS_MAX = 32;
|
|
25
|
+
var ProbeLane = class {
|
|
26
|
+
constructor(queue) {
|
|
27
|
+
this.queue = queue;
|
|
28
|
+
}
|
|
29
|
+
/** Queued items, oldest first; read by the queue's flush paths. */
|
|
30
|
+
items = [];
|
|
31
|
+
bytes = 0;
|
|
32
|
+
/** Probe items dropped on this load (caps, permanent rejection, exhausted retries, unload budget). */
|
|
33
|
+
dropped = 0;
|
|
34
|
+
/** Default probe page id (one per tracker instance) and the probe seq counter per page id. */
|
|
35
|
+
pageId;
|
|
36
|
+
seqs = /* @__PURE__ */ new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Queue a probe item. The payload is snapshotted (serialized once) so later host mutations cannot
|
|
39
|
+
* change it; items over {@link PROBE_PAYLOAD_MAX_BYTES} are dropped, and the oldest items are dropped
|
|
40
|
+
* beyond the queue caps. Arms the normal flush timer. Throws on unexpected errors (the queue logs).
|
|
41
|
+
*/
|
|
42
|
+
enqueue(entry, capturedSessionId) {
|
|
43
|
+
const payloadJson = JSON.stringify(entry.payload);
|
|
44
|
+
if (typeof payloadJson !== "string" || utf8Length(payloadJson) > PROBE_PAYLOAD_MAX_BYTES) {
|
|
45
|
+
this.dropped++;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const snapshot = { ...entry, payload: JSON.parse(payloadJson) };
|
|
49
|
+
const bytes = utf8Length(JSON.stringify(snapshot));
|
|
50
|
+
this.items.push({ sessionId: capturedSessionId, entry: snapshot, bytes, attempts: 0, flushAfter: 0 });
|
|
51
|
+
this.bytes += bytes;
|
|
52
|
+
while (this.items.length > PROBE_QUEUE_MAX_ITEMS || this.bytes > PROBE_QUEUE_MAX_BYTES && this.items.length > 1) {
|
|
53
|
+
const dropped = this.items.shift();
|
|
54
|
+
this.bytes -= dropped.bytes;
|
|
55
|
+
this.dropped++;
|
|
56
|
+
}
|
|
57
|
+
this.queue["scheduleFlush"](this.queue["flushInterval"](), "timer");
|
|
58
|
+
}
|
|
59
|
+
/** The probe part of one regular batch of `sessionId`. */
|
|
60
|
+
selectBatch(candidates, sessionId) {
|
|
61
|
+
return this.select(candidates, sessionId, PROBE_BATCH_MAX_ITEMS, PROBE_BATCH_MAX_BYTES);
|
|
62
|
+
}
|
|
63
|
+
/** Oldest-first selection for one session, within an item and byte cap (at least one item when any fits the item cap). */
|
|
64
|
+
select(candidates, sessionId, maxItems, maxBytes) {
|
|
65
|
+
const selected = [];
|
|
66
|
+
let bytes = 0;
|
|
67
|
+
for (const item of candidates) {
|
|
68
|
+
if (selected.length >= maxItems) break;
|
|
69
|
+
if (item.sessionId !== sessionId) continue;
|
|
70
|
+
if (bytes + item.bytes > maxBytes) {
|
|
71
|
+
if (selected.length === 0 && maxBytes >= PROBE_BATCH_MAX_BYTES) selected.push(item);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
bytes += item.bytes;
|
|
75
|
+
selected.push(item);
|
|
76
|
+
}
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
79
|
+
remove(items) {
|
|
80
|
+
if (items.length === 0) return;
|
|
81
|
+
const removed = new Set(items);
|
|
82
|
+
this.items = this.items.filter((item) => !removed.has(item));
|
|
83
|
+
for (const item of items) this.bytes -= item.bytes;
|
|
84
|
+
}
|
|
85
|
+
/** Drop `items` (all of them: `drop(lane.items.slice())`), counting them as dropped. */
|
|
86
|
+
drop(items) {
|
|
87
|
+
this.remove(items);
|
|
88
|
+
this.dropped += items.length;
|
|
89
|
+
}
|
|
90
|
+
/** Same policy as events: 2xx delivered, non-retryable dropped, retryable backed off up to `config.retry.maxAttempts`. */
|
|
91
|
+
handleResult(response, items, now) {
|
|
92
|
+
if (items.length === 0) return;
|
|
93
|
+
const queue = this.queue;
|
|
94
|
+
const status = response.statusCode;
|
|
95
|
+
if (typeof status === "number" && status >= 200 && status < 300) {
|
|
96
|
+
this.remove(items);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!queue["isRetryable"](status)) {
|
|
100
|
+
this.drop(items);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const maxAttempts = Math.max(1, queue["config"].retry.maxAttempts);
|
|
104
|
+
const retryAfterMs = queue["parseRetryAfter"](response);
|
|
105
|
+
const exhausted = [];
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
item.attempts += 1;
|
|
108
|
+
if (item.attempts >= maxAttempts) exhausted.push(item);
|
|
109
|
+
else item.flushAfter = now + queue["backoffForAttempt"](item.attempts, retryAfterMs);
|
|
110
|
+
}
|
|
111
|
+
this.drop(exhausted);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Unload / final teardown: one probe-only request for the current session within `budget` bytes,
|
|
115
|
+
* sent after the event request so the events' delivery is unaffected. Leftovers are dropped when
|
|
116
|
+
* `dropRest` (pagehide, destroy); otherwise they stay queued.
|
|
117
|
+
*/
|
|
118
|
+
sendUnload(budget, reason, dropRest, useBeacon) {
|
|
119
|
+
const queue = this.queue;
|
|
120
|
+
const sessionId = queue["sessionId"];
|
|
121
|
+
const envelope = { sessionId, events: [], sdk: queue["sdk"], probes: [] };
|
|
122
|
+
queue["attachBatchMeta"](envelope, [], reason);
|
|
123
|
+
const overhead = utf8Length(JSON.stringify(envelope)) + 120;
|
|
124
|
+
const items = budget > overhead ? this.select(this.items, sessionId, PROBE_BATCH_MAX_ITEMS, budget - overhead) : [];
|
|
125
|
+
if (items.length > 0) {
|
|
126
|
+
envelope.probes = items.map((item) => item.entry);
|
|
127
|
+
queue["attachBatchMeta"](envelope, items, reason);
|
|
128
|
+
const transport = queue["transport"];
|
|
129
|
+
if (useBeacon && queue["config"].beaconKeepalive && transport.sendBeacon) {
|
|
130
|
+
transport.sendBeacon(envelope);
|
|
131
|
+
} else {
|
|
132
|
+
void Promise.resolve(transport.send(envelope)).catch(() => void 0);
|
|
133
|
+
}
|
|
134
|
+
this.remove(items);
|
|
135
|
+
}
|
|
136
|
+
if (dropRest) this.drop(this.items.slice());
|
|
137
|
+
}
|
|
138
|
+
/** Default page id of probe items without one: one random id per lane (= per tracker instance). */
|
|
139
|
+
defaultPageId() {
|
|
140
|
+
return this.pageId ??= randomProbePageId();
|
|
141
|
+
}
|
|
142
|
+
/** Next probe seq of `pageId` (per-page counters, the oldest forgotten beyond {@link PROBE_PAGE_IDS_MAX}). */
|
|
143
|
+
nextSeq(pageId) {
|
|
144
|
+
const seq = this.seqs.get(pageId) ?? 0;
|
|
145
|
+
this.seqs.delete(pageId);
|
|
146
|
+
this.seqs.set(pageId, seq + 1);
|
|
147
|
+
if (this.seqs.size > PROBE_PAGE_IDS_MAX) {
|
|
148
|
+
const oldest = this.seqs.keys().next().value;
|
|
149
|
+
if (oldest !== void 0) this.seqs.delete(oldest);
|
|
150
|
+
}
|
|
151
|
+
return seq;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var PROBE_PAGE_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
155
|
+
function randomProbePageId() {
|
|
156
|
+
const bytes = new Uint8Array(16);
|
|
157
|
+
try {
|
|
158
|
+
crypto.getRandomValues(bytes);
|
|
159
|
+
} catch {
|
|
160
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
161
|
+
}
|
|
162
|
+
let out = "";
|
|
163
|
+
for (let i = 0; i < bytes.length; i++) out += PROBE_PAGE_ID_ALPHABET[bytes[i] & 63];
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
PROBE_BATCH_MAX_BYTES,
|
|
168
|
+
PROBE_BATCH_MAX_ITEMS,
|
|
169
|
+
PROBE_PAYLOAD_MAX_BYTES,
|
|
170
|
+
PROBE_QUEUE_MAX_BYTES,
|
|
171
|
+
PROBE_QUEUE_MAX_ITEMS,
|
|
172
|
+
ProbeLane,
|
|
173
|
+
UNLOAD_PROBE_MAX_BYTES,
|
|
174
|
+
installProbeLane
|
|
175
|
+
};
|