@genn-inc/cluebase-frontend-sdk 0.0.1
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/README.md +222 -0
- package/dist/_generated/correlation-attribute-keys.d.ts +2 -0
- package/dist/_generated/correlation-attribute-keys.js +33 -0
- package/dist/_generated/identity-pii-policy.d.ts +38 -0
- package/dist/_generated/identity-pii-policy.js +103 -0
- package/dist/_generated/ingest-privacy-tables.d.ts +120 -0
- package/dist/_generated/ingest-privacy-tables.js +890 -0
- package/dist/_generated/otel-resource-keys.d.ts +21 -0
- package/dist/_generated/otel-resource-keys.js +72 -0
- package/dist/_generated/otel-span-conversion.d.ts +48 -0
- package/dist/_generated/otel-span-conversion.js +69 -0
- package/dist/_generated/parameter-snapshot-leaf.d.ts +11 -0
- package/dist/_generated/parameter-snapshot-leaf.js +37 -0
- package/dist/_generated/sdk-runtime-policy.d.ts +7 -0
- package/dist/_generated/sdk-runtime-policy.js +14 -0
- package/dist/_generated/usage-governor-tables.d.ts +15 -0
- package/dist/_generated/usage-governor-tables.js +44 -0
- package/dist/context/browser-storage.d.ts +15 -0
- package/dist/context/browser-storage.js +38 -0
- package/dist/context/environment.d.ts +37 -0
- package/dist/context/environment.js +229 -0
- package/dist/context/identity.d.ts +16 -0
- package/dist/context/identity.js +139 -0
- package/dist/context/organization.d.ts +16 -0
- package/dist/context/organization.js +60 -0
- package/dist/context/session.d.ts +28 -0
- package/dist/context/session.js +130 -0
- package/dist/context/tab.d.ts +22 -0
- package/dist/context/tab.js +117 -0
- package/dist/context/trace.d.ts +76 -0
- package/dist/context/trace.js +163 -0
- package/dist/exporter/batch.d.ts +94 -0
- package/dist/exporter/batch.js +485 -0
- package/dist/exporter/browser-outbox-records.d.ts +159 -0
- package/dist/exporter/browser-outbox-records.js +341 -0
- package/dist/exporter/browser-outbox.d.ts +112 -0
- package/dist/exporter/browser-outbox.js +876 -0
- package/dist/exporter/client.d.ts +92 -0
- package/dist/exporter/client.js +541 -0
- package/dist/exporter/delivery-response-contract.d.ts +103 -0
- package/dist/exporter/delivery-response-contract.js +147 -0
- package/dist/exporter/delivery-response.d.ts +37 -0
- package/dist/exporter/delivery-response.js +151 -0
- package/dist/exporter/diagnostics.d.ts +42 -0
- package/dist/exporter/diagnostics.js +148 -0
- package/dist/exporter/event-id.d.ts +10 -0
- package/dist/exporter/event-id.js +75 -0
- package/dist/exporter/frontend-resource.d.ts +20 -0
- package/dist/exporter/frontend-resource.js +52 -0
- package/dist/exporter/local-minimization.d.ts +29 -0
- package/dist/exporter/local-minimization.js +87 -0
- package/dist/exporter/observation-source-event-exporter.d.ts +69 -0
- package/dist/exporter/observation-source-event-exporter.js +154 -0
- package/dist/exporter/parameter-snapshot.d.ts +14 -0
- package/dist/exporter/parameter-snapshot.js +215 -0
- package/dist/exporter/path-template.d.ts +1 -0
- package/dist/exporter/path-template.js +67 -0
- package/dist/exporter/raw-event-to-source-signal.d.ts +12 -0
- package/dist/exporter/raw-event-to-source-signal.js +297 -0
- package/dist/exporter/retry.d.ts +8 -0
- package/dist/exporter/retry.js +8 -0
- package/dist/exporter/sanitize.d.ts +30 -0
- package/dist/exporter/sanitize.js +177 -0
- package/dist/exporter/schema-evidence.d.ts +20 -0
- package/dist/exporter/schema-evidence.js +232 -0
- package/dist/exporter/source-event.d.ts +99 -0
- package/dist/exporter/source-event.js +13 -0
- package/dist/exporter/span-to-source-signal.d.ts +8 -0
- package/dist/exporter/span-to-source-signal.js +174 -0
- package/dist/exporter/text-fingerprint.d.ts +9 -0
- package/dist/exporter/text-fingerprint.js +16 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +158 -0
- package/dist/instrumentation/action.d.ts +2 -0
- package/dist/instrumentation/action.js +171 -0
- package/dist/instrumentation/cluebase-authored-attributes.d.ts +4 -0
- package/dist/instrumentation/cluebase-authored-attributes.js +32 -0
- package/dist/instrumentation/common.d.ts +73 -0
- package/dist/instrumentation/common.js +90 -0
- package/dist/instrumentation/dom.d.ts +63 -0
- package/dist/instrumentation/dom.js +640 -0
- package/dist/instrumentation/drag.d.ts +2 -0
- package/dist/instrumentation/drag.js +82 -0
- package/dist/instrumentation/error.d.ts +2 -0
- package/dist/instrumentation/error.js +318 -0
- package/dist/instrumentation/form.d.ts +2 -0
- package/dist/instrumentation/form.js +132 -0
- package/dist/instrumentation/input.d.ts +2 -0
- package/dist/instrumentation/input.js +180 -0
- package/dist/instrumentation/location.d.ts +11 -0
- package/dist/instrumentation/location.js +69 -0
- package/dist/instrumentation/naming-evidence.d.ts +20 -0
- package/dist/instrumentation/naming-evidence.js +11 -0
- package/dist/instrumentation/navigation.d.ts +2 -0
- package/dist/instrumentation/navigation.js +144 -0
- package/dist/instrumentation/network-request.d.ts +103 -0
- package/dist/instrumentation/network-request.js +285 -0
- package/dist/instrumentation/network-span-attributes.d.ts +25 -0
- package/dist/instrumentation/network-span-attributes.js +153 -0
- package/dist/instrumentation/network.d.ts +16 -0
- package/dist/instrumentation/network.js +502 -0
- package/dist/instrumentation/page.d.ts +2 -0
- package/dist/instrumentation/page.js +56 -0
- package/dist/instrumentation/performance.d.ts +2 -0
- package/dist/instrumentation/performance.js +365 -0
- package/dist/integrations/next-router.d.ts +16 -0
- package/dist/integrations/next-router.js +18 -0
- package/dist/integrations/react-router.d.ts +7 -0
- package/dist/integrations/react-router.js +61 -0
- package/dist/internal/internal-log.d.ts +23 -0
- package/dist/internal/internal-log.js +47 -0
- package/dist/internal/metrics.d.ts +10 -0
- package/dist/internal/metrics.js +50 -0
- package/dist/provider/_version.d.ts +1 -0
- package/dist/provider/_version.js +3 -0
- package/dist/provider/config.d.ts +12 -0
- package/dist/provider/config.js +177 -0
- package/dist/provider/constants.d.ts +62 -0
- package/dist/provider/constants.js +118 -0
- package/dist/provider/contracts.d.ts +51 -0
- package/dist/provider/contracts.js +46 -0
- package/dist/provider/privacy-policy-store.d.ts +20 -0
- package/dist/provider/privacy-policy-store.js +56 -0
- package/dist/provider/sampling.d.ts +2 -0
- package/dist/provider/sampling.js +17 -0
- package/dist/provider/sdk-event-pipeline.d.ts +60 -0
- package/dist/provider/sdk-event-pipeline.js +478 -0
- package/dist/provider/sdk-input-normalization.d.ts +7 -0
- package/dist/provider/sdk-input-normalization.js +113 -0
- package/dist/provider/sdk-otel-pipeline.d.ts +14 -0
- package/dist/provider/sdk-otel-pipeline.js +148 -0
- package/dist/provider/sdk-runtime-surfaces.d.ts +49 -0
- package/dist/provider/sdk-runtime-surfaces.js +216 -0
- package/dist/provider/sdk.d.ts +2 -0
- package/dist/provider/sdk.js +614 -0
- package/dist/provider/types.d.ts +323 -0
- package/dist/provider/types.js +1 -0
- package/dist/provider/usage-governor.d.ts +7 -0
- package/dist/provider/usage-governor.js +116 -0
- package/package.json +59 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CluebaseSanitizedTraits, CluebaseSubjectTraits, IdentityContext } from "../provider/types.js";
|
|
2
|
+
import { type StorageLike } from "./browser-storage.js";
|
|
3
|
+
export type ContextJsonTraits = CluebaseSanitizedTraits;
|
|
4
|
+
export type ContextTraitInput = CluebaseSubjectTraits;
|
|
5
|
+
export declare const sanitizeContextTraits: (traits: ContextTraitInput | undefined) => ContextJsonTraits;
|
|
6
|
+
export declare class IdentityManager {
|
|
7
|
+
private readonly storage;
|
|
8
|
+
private state;
|
|
9
|
+
constructor(storage?: StorageLike);
|
|
10
|
+
getContext(): IdentityContext;
|
|
11
|
+
identify(userId: string, traits?: ContextTraitInput): void;
|
|
12
|
+
clearUser(): void;
|
|
13
|
+
rotateAnonymousId(): string;
|
|
14
|
+
reset(): void;
|
|
15
|
+
private loadState;
|
|
16
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { generateAnonymousId } from "../exporter/event-id.js";
|
|
2
|
+
import { STORAGE_KEYS } from "../provider/constants.js";
|
|
3
|
+
import { resolveWebStorage, } from "./browser-storage.js";
|
|
4
|
+
const CONTEXT_ID_TRAIT_KEYS = new Set([
|
|
5
|
+
"accountId",
|
|
6
|
+
"account_id",
|
|
7
|
+
"workspaceId",
|
|
8
|
+
"workspace_id",
|
|
9
|
+
"organizationId",
|
|
10
|
+
"organization_id",
|
|
11
|
+
]);
|
|
12
|
+
const STRING_TRAIT_KEYS = new Set([
|
|
13
|
+
"name",
|
|
14
|
+
"email",
|
|
15
|
+
"emailAddress",
|
|
16
|
+
"displayName",
|
|
17
|
+
"avatarUrl",
|
|
18
|
+
]);
|
|
19
|
+
export const sanitizeContextTraits = (traits) => {
|
|
20
|
+
if (traits === undefined)
|
|
21
|
+
return {};
|
|
22
|
+
if (!isPlainRecord(traits)) {
|
|
23
|
+
throw new Error("identify traits must be a JSON object");
|
|
24
|
+
}
|
|
25
|
+
const sanitizedTraits = {};
|
|
26
|
+
for (const [key, value] of Object.entries(traits)) {
|
|
27
|
+
if (CONTEXT_ID_TRAIT_KEYS.has(key))
|
|
28
|
+
continue;
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
if (STRING_TRAIT_KEYS.has(key))
|
|
31
|
+
continue;
|
|
32
|
+
throw new Error(`identify trait ${key} must be JSON-serializable`);
|
|
33
|
+
}
|
|
34
|
+
if (STRING_TRAIT_KEYS.has(key) &&
|
|
35
|
+
value !== null &&
|
|
36
|
+
typeof value !== "string") {
|
|
37
|
+
throw new Error(`identify trait ${key} must be a string or null`);
|
|
38
|
+
}
|
|
39
|
+
if (!isCanonicalJsonValue(value)) {
|
|
40
|
+
throw new Error(`identify trait ${key} must be JSON-serializable`);
|
|
41
|
+
}
|
|
42
|
+
sanitizedTraits[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return sanitizedTraits;
|
|
45
|
+
};
|
|
46
|
+
const isCanonicalJsonValue = (value) => {
|
|
47
|
+
if (value === null)
|
|
48
|
+
return true;
|
|
49
|
+
const valueType = typeof value;
|
|
50
|
+
if (valueType === "string" ||
|
|
51
|
+
valueType === "number" ||
|
|
52
|
+
valueType === "boolean") {
|
|
53
|
+
return valueType !== "number" || Number.isFinite(value);
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value.every(isCanonicalJsonValue);
|
|
57
|
+
}
|
|
58
|
+
if (!isPlainRecord(value)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return Object.values(value).every(isCanonicalJsonValue);
|
|
62
|
+
};
|
|
63
|
+
const isPlainRecord = (value) => {
|
|
64
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const prototype = Object.getPrototypeOf(value);
|
|
68
|
+
return prototype === Object.prototype || prototype === null;
|
|
69
|
+
};
|
|
70
|
+
export class IdentityManager {
|
|
71
|
+
constructor(storage) {
|
|
72
|
+
this.storage = storage ?? resolveWebStorage("local", "cluebase:probe");
|
|
73
|
+
this.state = this.loadState();
|
|
74
|
+
}
|
|
75
|
+
getContext() {
|
|
76
|
+
return {
|
|
77
|
+
anonymousId: this.state.anonymousId,
|
|
78
|
+
userId: this.state.userId,
|
|
79
|
+
traits: { ...this.state.traits },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
identify(userId, traits) {
|
|
83
|
+
const sanitizedTraits = sanitizeContextTraits(traits);
|
|
84
|
+
this.state.userId = userId;
|
|
85
|
+
this.state.traits = {
|
|
86
|
+
...this.state.traits,
|
|
87
|
+
...sanitizedTraits,
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
this.storage.setItem(STORAGE_KEYS.userId, userId);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Swallow to keep SDK non-blocking.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
clearUser() {
|
|
97
|
+
this.state.userId = null;
|
|
98
|
+
this.state.traits = {};
|
|
99
|
+
try {
|
|
100
|
+
this.storage.removeItem(STORAGE_KEYS.userId);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Swallow to keep SDK non-blocking.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
rotateAnonymousId() {
|
|
107
|
+
const anonymousId = generateAnonymousId();
|
|
108
|
+
this.state.anonymousId = anonymousId;
|
|
109
|
+
try {
|
|
110
|
+
this.storage.setItem(STORAGE_KEYS.anonymousId, anonymousId);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Swallow to keep SDK non-blocking.
|
|
114
|
+
}
|
|
115
|
+
return anonymousId;
|
|
116
|
+
}
|
|
117
|
+
reset() {
|
|
118
|
+
this.clearUser();
|
|
119
|
+
this.rotateAnonymousId();
|
|
120
|
+
}
|
|
121
|
+
loadState() {
|
|
122
|
+
const storedAnonymousId = this.storage.getItem(STORAGE_KEYS.anonymousId);
|
|
123
|
+
const anonymousId = storedAnonymousId || generateAnonymousId();
|
|
124
|
+
if (!storedAnonymousId) {
|
|
125
|
+
try {
|
|
126
|
+
this.storage.setItem(STORAGE_KEYS.anonymousId, anonymousId);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Swallow to keep SDK non-blocking.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const userId = this.storage.getItem(STORAGE_KEYS.userId);
|
|
133
|
+
return {
|
|
134
|
+
anonymousId,
|
|
135
|
+
userId,
|
|
136
|
+
traits: {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OrganizationContext } from "../provider/types.js";
|
|
2
|
+
import { type ContextTraitInput } from "./identity.js";
|
|
3
|
+
import { type StorageLike } from "./browser-storage.js";
|
|
4
|
+
export declare class OrganizationManager {
|
|
5
|
+
private readonly storage;
|
|
6
|
+
private state;
|
|
7
|
+
constructor(storage?: StorageLike);
|
|
8
|
+
getContext(): OrganizationContext;
|
|
9
|
+
/**
|
|
10
|
+
* Set the MVP company context directly. Public `cluebase.group("organization")`
|
|
11
|
+
* must not create or mutate future account/workspace context.
|
|
12
|
+
*/
|
|
13
|
+
setOrganization(organizationId: string, traits?: ContextTraitInput): void;
|
|
14
|
+
reset(): void;
|
|
15
|
+
private loadState;
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { STORAGE_KEYS } from "../provider/constants.js";
|
|
2
|
+
import { sanitizeContextTraits, } from "./identity.js";
|
|
3
|
+
import { resolveWebStorage, } from "./browser-storage.js";
|
|
4
|
+
export class OrganizationManager {
|
|
5
|
+
constructor(storage) {
|
|
6
|
+
this.storage = storage ?? resolveWebStorage("local", "cluebase:organization_probe");
|
|
7
|
+
this.state = this.loadState();
|
|
8
|
+
}
|
|
9
|
+
getContext() {
|
|
10
|
+
return {
|
|
11
|
+
organizationId: this.state.organizationId,
|
|
12
|
+
traits: { ...this.state.traits },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Set the MVP company context directly. Public `cluebase.group("organization")`
|
|
17
|
+
* must not create or mutate future account/workspace context.
|
|
18
|
+
*/
|
|
19
|
+
setOrganization(organizationId, traits) {
|
|
20
|
+
const sanitizedTraits = sanitizeContextTraits(traits);
|
|
21
|
+
this.state.organizationId = organizationId;
|
|
22
|
+
this.state.traits = {
|
|
23
|
+
...this.state.traits,
|
|
24
|
+
...sanitizedTraits,
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
this.storage.setItem(STORAGE_KEYS.organizationId, organizationId);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Swallow to keep SDK non-blocking.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
reset() {
|
|
34
|
+
this.state = {
|
|
35
|
+
organizationId: null,
|
|
36
|
+
traits: {},
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
this.storage.removeItem(STORAGE_KEYS.organizationId);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Swallow to keep SDK non-blocking.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
loadState() {
|
|
46
|
+
const organizationId = this.storage.getItem(STORAGE_KEYS.organizationId);
|
|
47
|
+
try {
|
|
48
|
+
if (organizationId) {
|
|
49
|
+
this.storage.setItem(STORAGE_KEYS.organizationId, organizationId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Swallow to keep SDK non-blocking.
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
organizationId,
|
|
57
|
+
traits: {},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SessionContext } from "../provider/types.js";
|
|
2
|
+
import { type StorageLike } from "./browser-storage.js";
|
|
3
|
+
type SessionLifecycleResult = {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
started: boolean;
|
|
6
|
+
ended: {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
} | null;
|
|
10
|
+
};
|
|
11
|
+
export declare class SessionManager {
|
|
12
|
+
private readonly storage;
|
|
13
|
+
private readonly timeoutMs;
|
|
14
|
+
private context;
|
|
15
|
+
constructor(timeoutMs?: number, storage?: StorageLike);
|
|
16
|
+
getContext(): SessionContext;
|
|
17
|
+
ensureActiveSession(nowMs?: number): SessionLifecycleResult;
|
|
18
|
+
touch(nowMs?: number): void;
|
|
19
|
+
endSession(nowMs?: number): {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
} | null;
|
|
23
|
+
reset(nowMs?: number): string;
|
|
24
|
+
private startSession;
|
|
25
|
+
private clear;
|
|
26
|
+
private loadState;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { generateSessionId } from "../exporter/event-id.js";
|
|
2
|
+
import { DEFAULT_SESSION_TIMEOUT_MS, STORAGE_KEYS, } from "../provider/constants.js";
|
|
3
|
+
import { resolveWebStorage, } from "./browser-storage.js";
|
|
4
|
+
const parseMs = (value) => {
|
|
5
|
+
if (!value) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (Number.isFinite(parsed)) {
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
export class SessionManager {
|
|
15
|
+
constructor(timeoutMs = DEFAULT_SESSION_TIMEOUT_MS, storage) {
|
|
16
|
+
this.timeoutMs = timeoutMs;
|
|
17
|
+
this.storage = storage ?? resolveWebStorage("session", "cluebase:session_probe");
|
|
18
|
+
this.context = this.loadState();
|
|
19
|
+
}
|
|
20
|
+
getContext() {
|
|
21
|
+
return { ...this.context };
|
|
22
|
+
}
|
|
23
|
+
ensureActiveSession(nowMs = Date.now()) {
|
|
24
|
+
const currentSessionId = this.context.sessionId;
|
|
25
|
+
const currentStartedAt = this.context.startedAtMs;
|
|
26
|
+
const lastActivityAt = this.context.lastActivityAtMs;
|
|
27
|
+
if (!currentSessionId || !currentStartedAt || !lastActivityAt) {
|
|
28
|
+
const started = this.startSession(nowMs);
|
|
29
|
+
return {
|
|
30
|
+
sessionId: started,
|
|
31
|
+
started: true,
|
|
32
|
+
ended: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const idleMs = nowMs - lastActivityAt;
|
|
36
|
+
if (idleMs > this.timeoutMs) {
|
|
37
|
+
const endedDuration = Math.max(0, nowMs - currentStartedAt);
|
|
38
|
+
const ended = {
|
|
39
|
+
sessionId: currentSessionId,
|
|
40
|
+
durationMs: endedDuration,
|
|
41
|
+
};
|
|
42
|
+
const newSessionId = this.startSession(nowMs);
|
|
43
|
+
return {
|
|
44
|
+
sessionId: newSessionId,
|
|
45
|
+
started: true,
|
|
46
|
+
ended,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
this.touch(nowMs);
|
|
50
|
+
return {
|
|
51
|
+
sessionId: currentSessionId,
|
|
52
|
+
started: false,
|
|
53
|
+
ended: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
touch(nowMs = Date.now()) {
|
|
57
|
+
this.context.lastActivityAtMs = nowMs;
|
|
58
|
+
try {
|
|
59
|
+
this.storage.setItem(STORAGE_KEYS.sessionLastActivityAt, String(nowMs));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Swallow to keep SDK non-blocking.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
endSession(nowMs = Date.now()) {
|
|
66
|
+
if (!this.context.sessionId || !this.context.startedAtMs) {
|
|
67
|
+
this.clear();
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const ended = {
|
|
71
|
+
sessionId: this.context.sessionId,
|
|
72
|
+
durationMs: Math.max(0, nowMs - this.context.startedAtMs),
|
|
73
|
+
};
|
|
74
|
+
this.clear();
|
|
75
|
+
return ended;
|
|
76
|
+
}
|
|
77
|
+
reset(nowMs = Date.now()) {
|
|
78
|
+
this.clear();
|
|
79
|
+
return this.startSession(nowMs);
|
|
80
|
+
}
|
|
81
|
+
startSession(nowMs) {
|
|
82
|
+
const sessionId = generateSessionId();
|
|
83
|
+
this.context = {
|
|
84
|
+
sessionId,
|
|
85
|
+
startedAtMs: nowMs,
|
|
86
|
+
lastActivityAtMs: nowMs,
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
this.storage.setItem(STORAGE_KEYS.sessionId, sessionId);
|
|
90
|
+
this.storage.setItem(STORAGE_KEYS.sessionStartedAt, String(nowMs));
|
|
91
|
+
this.storage.setItem(STORAGE_KEYS.sessionLastActivityAt, String(nowMs));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Swallow to keep SDK non-blocking.
|
|
95
|
+
}
|
|
96
|
+
return sessionId;
|
|
97
|
+
}
|
|
98
|
+
clear() {
|
|
99
|
+
this.context = {
|
|
100
|
+
sessionId: null,
|
|
101
|
+
startedAtMs: null,
|
|
102
|
+
lastActivityAtMs: null,
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
this.storage.removeItem(STORAGE_KEYS.sessionId);
|
|
106
|
+
this.storage.removeItem(STORAGE_KEYS.sessionStartedAt);
|
|
107
|
+
this.storage.removeItem(STORAGE_KEYS.sessionLastActivityAt);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Swallow to keep SDK non-blocking.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
loadState() {
|
|
114
|
+
const sessionId = this.storage.getItem(STORAGE_KEYS.sessionId);
|
|
115
|
+
const startedAtMs = parseMs(this.storage.getItem(STORAGE_KEYS.sessionStartedAt));
|
|
116
|
+
const lastActivityAtMs = parseMs(this.storage.getItem(STORAGE_KEYS.sessionLastActivityAt));
|
|
117
|
+
if (!sessionId || !startedAtMs || !lastActivityAtMs) {
|
|
118
|
+
return {
|
|
119
|
+
sessionId: null,
|
|
120
|
+
startedAtMs: null,
|
|
121
|
+
lastActivityAtMs: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
sessionId,
|
|
126
|
+
startedAtMs,
|
|
127
|
+
lastActivityAtMs,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type StorageLike } from "./browser-storage.js";
|
|
2
|
+
type TabManagerOptions = {
|
|
3
|
+
claimStorage?: StorageLike | null;
|
|
4
|
+
documentClaimId?: string;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
bindLifecycleEvents?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare class TabManager {
|
|
9
|
+
private readonly storage;
|
|
10
|
+
private readonly claimStorage;
|
|
11
|
+
private readonly documentClaimId;
|
|
12
|
+
private readonly now;
|
|
13
|
+
private readonly bindLifecycleEvents;
|
|
14
|
+
private readonly tabId;
|
|
15
|
+
constructor(storage?: StorageLike, options?: TabManagerOptions);
|
|
16
|
+
getTabId(): string;
|
|
17
|
+
private loadOrCreateTabId;
|
|
18
|
+
private isClaimedByAnotherDocument;
|
|
19
|
+
private claimTabId;
|
|
20
|
+
private installLifecycleCleanup;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { generateEventId, generateTabId } from "../exporter/event-id.js";
|
|
2
|
+
import { STORAGE_KEYS } from "../provider/constants.js";
|
|
3
|
+
import { resolveWebStorage, } from "./browser-storage.js";
|
|
4
|
+
const TAB_CLAIM_KEY_PREFIX = "cluebase:tab_claim:";
|
|
5
|
+
const TAB_CLAIM_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
const DOCUMENT_CLAIM_ID = `doc_${generateEventId()}`;
|
|
7
|
+
const resolveClaimStorage = () => {
|
|
8
|
+
try {
|
|
9
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
10
|
+
const probeKey = "cluebase:tab_claim_probe";
|
|
11
|
+
globalThis.localStorage.setItem(probeKey, "1");
|
|
12
|
+
globalThis.localStorage.removeItem(probeKey);
|
|
13
|
+
return globalThis.localStorage;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Ignore and continue without duplicate-tab coordination.
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
const parseTabClaim = (value) => {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(value);
|
|
27
|
+
if (typeof parsed.claimId === "string" &&
|
|
28
|
+
typeof parsed.expiresAt === "number") {
|
|
29
|
+
return {
|
|
30
|
+
claimId: parsed.claimId,
|
|
31
|
+
expiresAt: parsed.expiresAt,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Ignore invalid claim records and let the current document reclaim it.
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
export class TabManager {
|
|
41
|
+
constructor(storage, options) {
|
|
42
|
+
this.storage = storage ?? resolveWebStorage("session", "cluebase:tab_probe");
|
|
43
|
+
this.claimStorage =
|
|
44
|
+
options && "claimStorage" in options
|
|
45
|
+
? (options.claimStorage ?? null)
|
|
46
|
+
: resolveClaimStorage();
|
|
47
|
+
this.documentClaimId = options?.documentClaimId ?? DOCUMENT_CLAIM_ID;
|
|
48
|
+
this.now = options?.now ?? Date.now;
|
|
49
|
+
this.bindLifecycleEvents = options?.bindLifecycleEvents ?? true;
|
|
50
|
+
this.tabId = this.loadOrCreateTabId();
|
|
51
|
+
this.claimTabId(this.tabId);
|
|
52
|
+
this.installLifecycleCleanup();
|
|
53
|
+
}
|
|
54
|
+
getTabId() {
|
|
55
|
+
return this.tabId;
|
|
56
|
+
}
|
|
57
|
+
loadOrCreateTabId() {
|
|
58
|
+
const stored = this.storage.getItem(STORAGE_KEYS.tabId);
|
|
59
|
+
if (stored && !this.isClaimedByAnotherDocument(stored)) {
|
|
60
|
+
return stored;
|
|
61
|
+
}
|
|
62
|
+
const tabId = generateTabId();
|
|
63
|
+
try {
|
|
64
|
+
this.storage.setItem(STORAGE_KEYS.tabId, tabId);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Swallow to keep SDK non-blocking.
|
|
68
|
+
}
|
|
69
|
+
return tabId;
|
|
70
|
+
}
|
|
71
|
+
isClaimedByAnotherDocument(tabId) {
|
|
72
|
+
if (!this.claimStorage) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const claim = parseTabClaim(this.claimStorage.getItem(`${TAB_CLAIM_KEY_PREFIX}${tabId}`));
|
|
76
|
+
if (!claim || claim.expiresAt <= this.now()) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return claim.claimId !== this.documentClaimId;
|
|
80
|
+
}
|
|
81
|
+
claimTabId(tabId) {
|
|
82
|
+
if (!this.claimStorage) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
this.claimStorage.setItem(`${TAB_CLAIM_KEY_PREFIX}${tabId}`, JSON.stringify({
|
|
87
|
+
claimId: this.documentClaimId,
|
|
88
|
+
expiresAt: this.now() + TAB_CLAIM_TTL_MS,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Swallow to keep SDK non-blocking.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
installLifecycleCleanup() {
|
|
96
|
+
if (!this.bindLifecycleEvents || !this.claimStorage) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
if (typeof globalThis.addEventListener !== "function") {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const cleanup = () => {
|
|
104
|
+
const claimKey = `${TAB_CLAIM_KEY_PREFIX}${this.tabId}`;
|
|
105
|
+
const claim = parseTabClaim(this.claimStorage?.getItem(claimKey) ?? null);
|
|
106
|
+
if (claim?.claimId === this.documentClaimId) {
|
|
107
|
+
this.claimStorage?.removeItem(claimKey);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
globalThis.addEventListener("pagehide", cleanup, { once: true });
|
|
111
|
+
globalThis.addEventListener("beforeunload", cleanup, { once: true });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Swallow to keep SDK non-blocking.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type TraceContextSnapshot = {
|
|
2
|
+
traceId: string;
|
|
3
|
+
activeSpanId: string | null;
|
|
4
|
+
};
|
|
5
|
+
export type RequestTraceContext = {
|
|
6
|
+
traceId: string;
|
|
7
|
+
parentSpanId: string | null;
|
|
8
|
+
requestId: string;
|
|
9
|
+
requestSpanId: string;
|
|
10
|
+
interactionId: string;
|
|
11
|
+
interactionInitiator: string | null;
|
|
12
|
+
interactionStartEventKind: string | null;
|
|
13
|
+
originFrontendInteractionId: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type ActiveInteractionSnapshot = {
|
|
16
|
+
interactionId: string;
|
|
17
|
+
interactionInitiator: string | null;
|
|
18
|
+
interactionStartEventKind: string | null;
|
|
19
|
+
targetStableKey: string | null;
|
|
20
|
+
formKey: string | null;
|
|
21
|
+
interactionScopeKind: string | null;
|
|
22
|
+
interactionScopeKey: string | null;
|
|
23
|
+
};
|
|
24
|
+
export type InteractionStartInput = {
|
|
25
|
+
interactionId?: string | null;
|
|
26
|
+
viewId?: string | null;
|
|
27
|
+
interactionInitiator?: string | null;
|
|
28
|
+
interactionStartEventKind?: string | null;
|
|
29
|
+
targetStableKey?: string | null;
|
|
30
|
+
formKey?: string | null;
|
|
31
|
+
interactionScopeKind?: string | null;
|
|
32
|
+
interactionScopeKey?: string | null;
|
|
33
|
+
nowMs?: number;
|
|
34
|
+
};
|
|
35
|
+
type FrontendActionInput = InteractionStartInput & {
|
|
36
|
+
viewId?: string | null;
|
|
37
|
+
};
|
|
38
|
+
export declare class TraceManager {
|
|
39
|
+
private traceId;
|
|
40
|
+
private activeSpanId;
|
|
41
|
+
private activeInteraction;
|
|
42
|
+
private recentInteraction;
|
|
43
|
+
private latestTrigger;
|
|
44
|
+
constructor();
|
|
45
|
+
reset(options?: {
|
|
46
|
+
preserveActiveInteraction?: boolean;
|
|
47
|
+
}): void;
|
|
48
|
+
getSnapshot(): TraceContextSnapshot;
|
|
49
|
+
startRootSpan(): {
|
|
50
|
+
traceId: string;
|
|
51
|
+
spanId: string;
|
|
52
|
+
};
|
|
53
|
+
setActiveSpan(spanId: string | null): void;
|
|
54
|
+
startFrontendAction(input?: FrontendActionInput): string;
|
|
55
|
+
endFrontendAction(interactionId?: string | null): void;
|
|
56
|
+
private getOrStartFrontendAction;
|
|
57
|
+
private recordTrigger;
|
|
58
|
+
startInteraction(input: InteractionStartInput): string;
|
|
59
|
+
joinOrStartInteraction(input: InteractionStartInput): string;
|
|
60
|
+
joinClickInteractionForSubmit(input: {
|
|
61
|
+
formKey: string | null;
|
|
62
|
+
nowMs?: number;
|
|
63
|
+
}): string | null;
|
|
64
|
+
getActiveInteractionId(): string | null;
|
|
65
|
+
getActiveInteractionSnapshot(): ActiveInteractionSnapshot | null;
|
|
66
|
+
getRecentInteractionSnapshot(nowMs: number | undefined, maxAgeMs: number): ActiveInteractionSnapshot | null;
|
|
67
|
+
createRequestContext(input?: {
|
|
68
|
+
requestId?: string;
|
|
69
|
+
method?: string | null;
|
|
70
|
+
urlKey?: string | null;
|
|
71
|
+
nowMs?: number;
|
|
72
|
+
}): RequestTraceContext;
|
|
73
|
+
private getActiveInteractionState;
|
|
74
|
+
private snapshotOf;
|
|
75
|
+
}
|
|
76
|
+
export {};
|