@maple-dev/browser 0.1.0 → 0.3.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/README.md +18 -5
- package/dist/index.d.mts +9 -4
- package/dist/index.mjs +360 -243
- package/package.json +13 -12
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ npm install @maple-dev/browser
|
|
|
17
17
|
import { MapleBrowser } from "@maple-dev/browser"
|
|
18
18
|
|
|
19
19
|
MapleBrowser.init({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
ingestKey: "maple_pk_...", // public ingest key
|
|
21
|
+
serviceName: "acme-web",
|
|
22
|
+
environment: "production",
|
|
23
|
+
replay: { enabled: true, sampleRate: 1.0 },
|
|
24
|
+
privacy: { maskAllInputs: true },
|
|
25
25
|
})
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -35,6 +35,19 @@ That single call:
|
|
|
35
35
|
- writes session metadata at start (`active`) and on page hide (`ended`),
|
|
36
36
|
including the trace ids observed during the session.
|
|
37
37
|
|
|
38
|
+
## Identifying users
|
|
39
|
+
|
|
40
|
+
Pass `userId` to `init()` when you already know the signed-in user, or call
|
|
41
|
+
`MapleBrowser.identify(user.id)` later. The id is attached to future session
|
|
42
|
+
metadata rows and stamped as `user.id` on future browser-created spans.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
MapleBrowser.identify(user.id)
|
|
46
|
+
|
|
47
|
+
// after sign-out
|
|
48
|
+
MapleBrowser.identify(null)
|
|
49
|
+
```
|
|
50
|
+
|
|
38
51
|
## Privacy
|
|
39
52
|
|
|
40
53
|
`maskAllInputs` (default **on**) masks every `<input>` value. Use rrweb's
|
package/dist/index.d.mts
CHANGED
|
@@ -7,12 +7,17 @@ interface MapleBrowserConfig {
|
|
|
7
7
|
readonly serviceName: string;
|
|
8
8
|
/** Maple ingest base URL. Defaults to `https://ingest.maple.dev`. */
|
|
9
9
|
readonly endpoint?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Logical group this service belongs to, emitted as the OTel
|
|
12
|
+
* `service.namespace` resource attribute on traces. Optional.
|
|
13
|
+
*/
|
|
14
|
+
readonly serviceNamespace?: string;
|
|
10
15
|
/** Service version / commit SHA. */
|
|
11
16
|
readonly serviceVersion?: string;
|
|
12
17
|
/** Deployment environment, e.g. "production". */
|
|
13
18
|
readonly environment?: string;
|
|
14
|
-
/** Optional user id attached to
|
|
15
|
-
readonly userId?: string;
|
|
19
|
+
/** Optional user id attached to replay sessions and future browser spans. */
|
|
20
|
+
readonly userId?: string | null | undefined;
|
|
16
21
|
readonly tracing?: {
|
|
17
22
|
/** Default true. */readonly enabled?: boolean;
|
|
18
23
|
/**
|
|
@@ -60,8 +65,8 @@ interface MapleBrowserHandle {
|
|
|
60
65
|
* ```
|
|
61
66
|
*/
|
|
62
67
|
declare const MapleBrowser: {
|
|
63
|
-
init: (config: MapleBrowserConfig) => MapleBrowserHandle; /** Attach
|
|
64
|
-
identify: (userId
|
|
68
|
+
init: (config: MapleBrowserConfig) => MapleBrowserHandle; /** Attach, replace, or clear the user id on the active session. Safe to call repeatedly. */
|
|
69
|
+
identify: (userId?: string | null) => void;
|
|
65
70
|
};
|
|
66
71
|
//#endregion
|
|
67
72
|
export { MapleBrowser, type MapleBrowserConfig, type MapleBrowserHandle };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { trace } from "@opentelemetry/api";
|
|
2
|
+
import { record } from "rrweb";
|
|
1
3
|
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
2
4
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
3
5
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
@@ -5,33 +7,7 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
|
5
7
|
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
6
8
|
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
|
|
7
9
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
8
|
-
|
|
9
|
-
import { trace } from "@opentelemetry/api";
|
|
10
|
-
//#region src/config.ts
|
|
11
|
-
const DEFAULT_ENDPOINT = "https://ingest.maple.dev";
|
|
12
|
-
function resolveConfig(config) {
|
|
13
|
-
return {
|
|
14
|
-
ingestKey: config.ingestKey,
|
|
15
|
-
serviceName: config.serviceName,
|
|
16
|
-
endpoint: (config.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
17
|
-
serviceVersion: config.serviceVersion,
|
|
18
|
-
environment: config.environment,
|
|
19
|
-
userId: config.userId,
|
|
20
|
-
tracingEnabled: config.tracing?.enabled ?? true,
|
|
21
|
-
tracingInstrumentFetch: config.tracing?.instrumentFetch ?? true,
|
|
22
|
-
replayEnabled: config.replay?.enabled ?? true,
|
|
23
|
-
replaySampleRate: config.replay?.sampleRate ?? 1,
|
|
24
|
-
maskAllInputs: config.privacy?.maskAllInputs ?? true,
|
|
25
|
-
maskAllText: config.privacy?.maskAllText ?? false
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/** ClickHouse-style `YYYY-MM-DD HH:MM:SS.mmm` in UTC (matches the ingest gateway). */
|
|
29
|
-
function formatCHDateTime(date) {
|
|
30
|
-
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
31
|
-
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}.${pad(date.getUTCMilliseconds(), 3)}`;
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/session.ts
|
|
10
|
+
//#region ../browser-session/src/session.ts
|
|
35
11
|
const STORAGE_KEY = "maple.session";
|
|
36
12
|
/** Rotate the session after this much inactivity (PostHog's default). */
|
|
37
13
|
const IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
@@ -44,7 +20,8 @@ function freshRecord(now) {
|
|
|
44
20
|
id: crypto.randomUUID(),
|
|
45
21
|
startedAt: now,
|
|
46
22
|
lastActivityAt: now,
|
|
47
|
-
chunkSeq: 0
|
|
23
|
+
chunkSeq: 0,
|
|
24
|
+
metaVersion: 0
|
|
48
25
|
};
|
|
49
26
|
}
|
|
50
27
|
function readRecord() {
|
|
@@ -106,16 +83,26 @@ function nextChunkSeq() {
|
|
|
106
83
|
});
|
|
107
84
|
return seq;
|
|
108
85
|
}
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Take the next session-metadata row version for the current session.
|
|
88
|
+
* Monotonic per session across reloads, hide/resume cycles, and writers (both
|
|
89
|
+
* SDKs share the persisted counter), so `argMax(field, Version)` on the
|
|
90
|
+
* backend always resolves to the most recently posted row. Records written by
|
|
91
|
+
* older SDKs (no `metaVersion`) already posted versions 1 and 2, so the
|
|
92
|
+
* counter resumes at 3 for them; a fresh session starts at 1.
|
|
93
|
+
*/
|
|
94
|
+
function nextMetaVersion() {
|
|
95
|
+
const record = readRecord() ?? freshRecord(Date.now());
|
|
96
|
+
const version = (record.metaVersion ?? 2) + 1;
|
|
97
|
+
writeRecord({
|
|
98
|
+
...record,
|
|
99
|
+
metaVersion: version
|
|
100
|
+
});
|
|
101
|
+
return version;
|
|
116
102
|
}
|
|
117
103
|
//#endregion
|
|
118
|
-
//#region src/
|
|
104
|
+
//#region ../browser-session/src/sink.ts
|
|
105
|
+
const SESSION_SINK_KEY = "__MAPLE_BROWSER_SESSION__";
|
|
119
106
|
const observedTraceIds = /* @__PURE__ */ new Set();
|
|
120
107
|
/** Record a trace id seen during the session. Idempotent per id. */
|
|
121
108
|
function recordTraceId(traceId) {
|
|
@@ -124,12 +111,11 @@ function recordTraceId(traceId) {
|
|
|
124
111
|
function getObservedTraceIds() {
|
|
125
112
|
return Array.from(observedTraceIds);
|
|
126
113
|
}
|
|
127
|
-
const SESSION_SINK_KEY = "__MAPLE_BROWSER_SESSION__";
|
|
128
114
|
/**
|
|
129
|
-
* Publish the session sink on `globalThis` so other tracers in the page
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* side, so init ordering between
|
|
115
|
+
* Publish the session sink on `globalThis` so other tracers in the page can
|
|
116
|
+
* attach their trace ids to the active replay session without a direct
|
|
117
|
+
* dependency on the publishing SDK. Reads are lazy/per-span on the consumer
|
|
118
|
+
* side, so init ordering between SDKs does not matter.
|
|
133
119
|
*/
|
|
134
120
|
function publishSessionSink(sessionId) {
|
|
135
121
|
globalThis[SESSION_SINK_KEY] = {
|
|
@@ -138,62 +124,63 @@ function publishSessionSink(sessionId) {
|
|
|
138
124
|
};
|
|
139
125
|
}
|
|
140
126
|
//#endregion
|
|
141
|
-
//#region src/
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
};
|
|
127
|
+
//#region ../browser-session/src/user-agent.ts
|
|
128
|
+
/** Best-effort UA parse — enough to populate filterable session facets. */
|
|
129
|
+
function parseUserAgent(ua) {
|
|
130
|
+
return {
|
|
131
|
+
browserName: /edg/i.test(ua) ? "Edge" : /opr|opera/i.test(ua) ? "Opera" : /chrome|crios/i.test(ua) ? "Chrome" : /firefox|fxios/i.test(ua) ? "Firefox" : /safari/i.test(ua) ? "Safari" : "Unknown",
|
|
132
|
+
osName: /windows/i.test(ua) ? "Windows" : /iphone|ipad|ios/i.test(ua) ? "iOS" : /mac os|macintosh/i.test(ua) ? "macOS" : /android/i.test(ua) ? "Android" : /linux/i.test(ua) ? "Linux" : "Unknown",
|
|
133
|
+
deviceType: /mobile|iphone|android.*mobile/i.test(ua) ? "mobile" : /ipad|tablet/i.test(ua) ? "tablet" : "desktop"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region ../browser-session/src/meta-row.ts
|
|
138
|
+
/** ClickHouse-style `YYYY-MM-DD HH:MM:SS.mmm` in UTC (matches the ingest gateway). */
|
|
139
|
+
function formatCHDateTime(date) {
|
|
140
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
141
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}.${pad(date.getUTCMilliseconds(), 3)}`;
|
|
142
|
+
}
|
|
158
143
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* requests — that tracer feeds the session via the published sink instead, and
|
|
164
|
-
* this avoids redundant duplicate network spans. Returns a shutdown function.
|
|
144
|
+
* Build one `/v1/sessionReplays/meta` NDJSON row. Shared by `@maple-dev/browser`
|
|
145
|
+
* and the Effect client SDK so a session looks identical no matter which SDK
|
|
146
|
+
* posted it. UA/URL facets come from the live browser globals; absent (tests,
|
|
147
|
+
* exotic embedders) they fall back to empty strings.
|
|
165
148
|
*/
|
|
166
|
-
function
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
149
|
+
function buildSessionMetaRow(input) {
|
|
150
|
+
const g = globalThis;
|
|
151
|
+
const userAgent = g["navigator"]?.userAgent ?? "";
|
|
152
|
+
const ua = parseUserAgent(userAgent);
|
|
153
|
+
const now = /* @__PURE__ */ new Date();
|
|
154
|
+
const row = {
|
|
155
|
+
session_id: input.sessionId,
|
|
156
|
+
start_time: formatCHDateTime(input.startedAt),
|
|
157
|
+
status: input.status,
|
|
158
|
+
version: input.version,
|
|
159
|
+
user_id: input.userId ?? "",
|
|
160
|
+
url_initial: g["window"]?.location?.href ?? "",
|
|
161
|
+
user_agent: userAgent,
|
|
162
|
+
browser_name: ua.browserName,
|
|
163
|
+
os_name: ua.osName,
|
|
164
|
+
device_type: ua.deviceType,
|
|
165
|
+
service_name: input.serviceName,
|
|
166
|
+
resource_attributes: {
|
|
167
|
+
...input.environment ? {
|
|
168
|
+
"deployment.environment": input.environment,
|
|
169
|
+
"deployment.environment.name": input.environment
|
|
170
|
+
} : {},
|
|
171
|
+
...input.serviceVersion ? { "deployment.commit_sha": input.serviceVersion } : {}
|
|
172
|
+
}
|
|
171
173
|
};
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
attributes["deployment.environment"] = config.environment;
|
|
178
|
-
attributes["deployment.environment.name"] = config.environment;
|
|
174
|
+
if (input.status === "ended") {
|
|
175
|
+
row.end_time = formatCHDateTime(now);
|
|
176
|
+
row.duration_ms = Math.max(0, now.getTime() - input.startedAt.getTime());
|
|
177
|
+
row.click_count = input.clickCount ?? 0;
|
|
178
|
+
row.trace_ids = input.traceIds ? Array.from(input.traceIds) : [];
|
|
179
179
|
}
|
|
180
|
-
|
|
181
|
-
url: `${config.endpoint}/v1/traces`,
|
|
182
|
-
headers: { Authorization: `Bearer ${config.ingestKey}` }
|
|
183
|
-
});
|
|
184
|
-
const provider = new WebTracerProvider({
|
|
185
|
-
resource: resourceFromAttributes(attributes),
|
|
186
|
-
spanProcessors: [new TraceIdCollector(), new BatchSpanProcessor(exporter)]
|
|
187
|
-
});
|
|
188
|
-
provider.register();
|
|
189
|
-
if (config.tracingInstrumentFetch) registerInstrumentations({ instrumentations: [new FetchInstrumentation({ ignoreUrls: [new RegExp(`${escapeRegExp(config.endpoint)}/v1/`)] })] });
|
|
190
|
-
return () => provider.shutdown();
|
|
191
|
-
}
|
|
192
|
-
function escapeRegExp(value) {
|
|
193
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
180
|
+
return row;
|
|
194
181
|
}
|
|
195
182
|
//#endregion
|
|
196
|
-
//#region src/replay/transport.ts
|
|
183
|
+
//#region ../browser-session/src/replay/transport.ts
|
|
197
184
|
let lastWarnAt = 0;
|
|
198
185
|
function warnDropped(what, error) {
|
|
199
186
|
const now = Date.now();
|
|
@@ -261,75 +248,7 @@ async function postSessionBlob(config, meta, gzipped, keepalive = false) {
|
|
|
261
248
|
});
|
|
262
249
|
}
|
|
263
250
|
//#endregion
|
|
264
|
-
//#region src/replay/
|
|
265
|
-
/** Approximate byte size of an event for flush-threshold accounting. Falls back
|
|
266
|
-
* to a fixed estimate for values that can't be serialized (e.g. cycles). */
|
|
267
|
-
function approximateSize(value) {
|
|
268
|
-
try {
|
|
269
|
-
return JSON.stringify(value).length;
|
|
270
|
-
} catch {
|
|
271
|
-
return 256;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region src/replay/record.ts
|
|
276
|
-
const FULL_SNAPSHOT = 2;
|
|
277
|
-
const INCREMENTAL = 3;
|
|
278
|
-
const SOURCE_MOUSE_INTERACTION = 2;
|
|
279
|
-
const MOUSE_CLICK = 2;
|
|
280
|
-
const FLUSH_INTERVAL_MS$1 = 5e3;
|
|
281
|
-
const FLUSH_BYTES$1 = 100 * 1024;
|
|
282
|
-
const CHECKOUT_EVERY_MS = 3e4;
|
|
283
|
-
function startRecording(config, sessionId) {
|
|
284
|
-
let buffer = [];
|
|
285
|
-
let bufferBytes = 0;
|
|
286
|
-
let bufferHasCheckpoint = false;
|
|
287
|
-
let clickCount = 0;
|
|
288
|
-
const flush = async (keepalive = false) => {
|
|
289
|
-
if (buffer.length === 0) return;
|
|
290
|
-
markActivity();
|
|
291
|
-
const events = buffer;
|
|
292
|
-
const isCheckpoint = bufferHasCheckpoint;
|
|
293
|
-
const seq = nextChunkSeq();
|
|
294
|
-
const first = events[0].timestamp;
|
|
295
|
-
const last = events[events.length - 1].timestamp;
|
|
296
|
-
buffer = [];
|
|
297
|
-
bufferBytes = 0;
|
|
298
|
-
bufferHasCheckpoint = false;
|
|
299
|
-
const gzipped = await gzip(new TextEncoder().encode(JSON.stringify(events)));
|
|
300
|
-
await postSessionBlob(config, {
|
|
301
|
-
sessionId,
|
|
302
|
-
chunkSeq: seq,
|
|
303
|
-
isCheckpoint,
|
|
304
|
-
eventCount: events.length,
|
|
305
|
-
durationMs: Math.max(0, last - first)
|
|
306
|
-
}, gzipped, keepalive);
|
|
307
|
-
};
|
|
308
|
-
const stop = record({
|
|
309
|
-
emit: (event, isCheckpoint) => {
|
|
310
|
-
const e = event;
|
|
311
|
-
if (isCheckpoint === true || e.type === FULL_SNAPSHOT) bufferHasCheckpoint = true;
|
|
312
|
-
if (e.type === INCREMENTAL && e.data?.source === SOURCE_MOUSE_INTERACTION && e.data.type === MOUSE_CLICK) clickCount++;
|
|
313
|
-
buffer.push(e);
|
|
314
|
-
bufferBytes += approximateSize(e);
|
|
315
|
-
if (bufferBytes >= FLUSH_BYTES$1) flush();
|
|
316
|
-
},
|
|
317
|
-
maskAllInputs: config.maskAllInputs,
|
|
318
|
-
...config.maskAllText ? { maskTextSelector: "*" } : {},
|
|
319
|
-
checkoutEveryNms: CHECKOUT_EVERY_MS
|
|
320
|
-
});
|
|
321
|
-
const flushTimer = setInterval(() => void flush(), FLUSH_INTERVAL_MS$1);
|
|
322
|
-
return {
|
|
323
|
-
stop: () => {
|
|
324
|
-
clearInterval(flushTimer);
|
|
325
|
-
stop?.();
|
|
326
|
-
},
|
|
327
|
-
flush,
|
|
328
|
-
getClickCount: () => clickCount
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
//#endregion
|
|
332
|
-
//#region src/replay/capture/shared.ts
|
|
251
|
+
//#region ../browser-session/src/replay/capture/shared.ts
|
|
333
252
|
/** Emit best-effort: capture must never throw into the host app's call site. */
|
|
334
253
|
function safeEmit(emit, ev) {
|
|
335
254
|
try {
|
|
@@ -337,7 +256,7 @@ function safeEmit(emit, ev) {
|
|
|
337
256
|
} catch {}
|
|
338
257
|
}
|
|
339
258
|
//#endregion
|
|
340
|
-
//#region src/replay/capture/console.ts
|
|
259
|
+
//#region ../browser-session/src/replay/capture/console.ts
|
|
341
260
|
const LEVELS = [
|
|
342
261
|
"log",
|
|
343
262
|
"info",
|
|
@@ -385,7 +304,7 @@ function formatArgs(args) {
|
|
|
385
304
|
return text.length > MAX_MESSAGE ? `${text.slice(0, MAX_MESSAGE)}…` : text;
|
|
386
305
|
}
|
|
387
306
|
//#endregion
|
|
388
|
-
//#region src/replay/capture/network.ts
|
|
307
|
+
//#region ../browser-session/src/replay/capture/network.ts
|
|
389
308
|
/**
|
|
390
309
|
* Capture fetch + XHR requests as session events, tagged with the active trace
|
|
391
310
|
* id so each request links to its backend trace. `ignoreUrl` skips Maple's own
|
|
@@ -459,7 +378,7 @@ function requestMethod(input, init) {
|
|
|
459
378
|
return (init?.method ?? (typeof input === "object" && "method" in input ? input.method : void 0) ?? "GET").toUpperCase();
|
|
460
379
|
}
|
|
461
380
|
//#endregion
|
|
462
|
-
//#region src/replay/capture/errors.ts
|
|
381
|
+
//#region ../browser-session/src/replay/capture/errors.ts
|
|
463
382
|
const MAX_STACK = 4e3;
|
|
464
383
|
/** Capture uncaught errors + unhandled promise rejections as session events. */
|
|
465
384
|
function installErrorCapture(emit) {
|
|
@@ -494,7 +413,7 @@ function truncate(stack) {
|
|
|
494
413
|
return stack.length > MAX_STACK ? `${stack.slice(0, MAX_STACK)}…` : stack;
|
|
495
414
|
}
|
|
496
415
|
//#endregion
|
|
497
|
-
//#region src/replay/capture/navigation.ts
|
|
416
|
+
//#region ../browser-session/src/replay/capture/navigation.ts
|
|
498
417
|
/**
|
|
499
418
|
* Capture page views as session events: the initial load plus every SPA
|
|
500
419
|
* navigation (history pushState/replaceState, popstate, hashchange).
|
|
@@ -533,7 +452,7 @@ function installNavigationCapture(emit) {
|
|
|
533
452
|
};
|
|
534
453
|
}
|
|
535
454
|
//#endregion
|
|
536
|
-
//#region src/replay/capture/interactions.ts
|
|
455
|
+
//#region ../browser-session/src/replay/capture/interactions.ts
|
|
537
456
|
const MAX_TEXT = 120;
|
|
538
457
|
/**
|
|
539
458
|
* Capture clicks and input events as session events. Listens in the capture
|
|
@@ -576,13 +495,29 @@ function textOf(el) {
|
|
|
576
495
|
return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text;
|
|
577
496
|
}
|
|
578
497
|
//#endregion
|
|
579
|
-
//#region src/replay/
|
|
580
|
-
|
|
581
|
-
|
|
498
|
+
//#region ../browser-session/src/replay/util.ts
|
|
499
|
+
/** Approximate byte size of an event for flush-threshold accounting. Falls back
|
|
500
|
+
* to a fixed estimate for values that can't be serialized (e.g. cycles). */
|
|
501
|
+
function approximateSize(value) {
|
|
502
|
+
try {
|
|
503
|
+
return JSON.stringify(value).length;
|
|
504
|
+
} catch {
|
|
505
|
+
return 256;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region ../browser-session/src/replay/events.ts
|
|
510
|
+
const FLUSH_INTERVAL_MS$1 = 5e3;
|
|
511
|
+
const FLUSH_BYTES$1 = 64 * 1024;
|
|
582
512
|
const ZERO_TRACE_ID = "00000000000000000000000000000000";
|
|
583
|
-
|
|
513
|
+
let traceIdProvider = () => void 0;
|
|
514
|
+
/** Wire the host SDK's active-trace-id lookup into event capture. */
|
|
515
|
+
function setActiveTraceIdProvider(provider) {
|
|
516
|
+
traceIdProvider = provider;
|
|
517
|
+
}
|
|
518
|
+
/** The trace id of the active span, or undefined when none is active. */
|
|
584
519
|
function activeTraceId() {
|
|
585
|
-
const id =
|
|
520
|
+
const id = traceIdProvider();
|
|
586
521
|
return id && id !== ZERO_TRACE_ID ? id : void 0;
|
|
587
522
|
}
|
|
588
523
|
/**
|
|
@@ -600,7 +535,7 @@ function startEventCapture(config, sessionId) {
|
|
|
600
535
|
seq: seq++
|
|
601
536
|
});
|
|
602
537
|
bufferBytes += approximateSize(ev);
|
|
603
|
-
if (bufferBytes >= FLUSH_BYTES) flush();
|
|
538
|
+
if (bufferBytes >= FLUSH_BYTES$1) flush();
|
|
604
539
|
};
|
|
605
540
|
const flush = async (keepalive = false) => {
|
|
606
541
|
if (buffer.length === 0) return;
|
|
@@ -618,7 +553,7 @@ function startEventCapture(config, sessionId) {
|
|
|
618
553
|
installNetworkCapture(emit, ignoreUrl),
|
|
619
554
|
installErrorCapture(emit)
|
|
620
555
|
];
|
|
621
|
-
const flushTimer = setInterval(() => void flush(), FLUSH_INTERVAL_MS);
|
|
556
|
+
const flushTimer = setInterval(() => void flush(), FLUSH_INTERVAL_MS$1);
|
|
622
557
|
return {
|
|
623
558
|
stop: () => {
|
|
624
559
|
clearInterval(flushTimer);
|
|
@@ -649,6 +584,234 @@ function toRow(sessionId, ev, seq) {
|
|
|
649
584
|
};
|
|
650
585
|
}
|
|
651
586
|
//#endregion
|
|
587
|
+
//#region ../browser-session/src/replay/record.ts
|
|
588
|
+
const FULL_SNAPSHOT = 2;
|
|
589
|
+
const INCREMENTAL = 3;
|
|
590
|
+
const SOURCE_MOUSE_INTERACTION = 2;
|
|
591
|
+
const MOUSE_CLICK = 2;
|
|
592
|
+
const FLUSH_INTERVAL_MS = 5e3;
|
|
593
|
+
const FLUSH_BYTES = 100 * 1024;
|
|
594
|
+
const CHECKOUT_EVERY_MS = 3e4;
|
|
595
|
+
function startRecording(config, sessionId) {
|
|
596
|
+
let buffer = [];
|
|
597
|
+
let bufferBytes = 0;
|
|
598
|
+
let bufferHasCheckpoint = false;
|
|
599
|
+
let clickCount = 0;
|
|
600
|
+
const flush = async (keepalive = false) => {
|
|
601
|
+
if (buffer.length === 0) return;
|
|
602
|
+
markActivity();
|
|
603
|
+
const events = buffer;
|
|
604
|
+
const isCheckpoint = bufferHasCheckpoint;
|
|
605
|
+
const seq = nextChunkSeq();
|
|
606
|
+
const first = events[0].timestamp;
|
|
607
|
+
const last = events[events.length - 1].timestamp;
|
|
608
|
+
buffer = [];
|
|
609
|
+
bufferBytes = 0;
|
|
610
|
+
bufferHasCheckpoint = false;
|
|
611
|
+
const gzipped = await gzip(new TextEncoder().encode(JSON.stringify(events)));
|
|
612
|
+
await postSessionBlob(config, {
|
|
613
|
+
sessionId,
|
|
614
|
+
chunkSeq: seq,
|
|
615
|
+
isCheckpoint,
|
|
616
|
+
eventCount: events.length,
|
|
617
|
+
durationMs: Math.max(0, last - first)
|
|
618
|
+
}, gzipped, keepalive);
|
|
619
|
+
};
|
|
620
|
+
const stop = record({
|
|
621
|
+
emit: (event, isCheckpoint) => {
|
|
622
|
+
const e = event;
|
|
623
|
+
if (isCheckpoint === true || e.type === FULL_SNAPSHOT) bufferHasCheckpoint = true;
|
|
624
|
+
if (e.type === INCREMENTAL && e.data?.source === SOURCE_MOUSE_INTERACTION && e.data.type === MOUSE_CLICK) clickCount++;
|
|
625
|
+
buffer.push(e);
|
|
626
|
+
bufferBytes += approximateSize(e);
|
|
627
|
+
if (bufferBytes >= FLUSH_BYTES) flush();
|
|
628
|
+
},
|
|
629
|
+
maskAllInputs: config.maskAllInputs,
|
|
630
|
+
...config.maskAllText ? { maskTextSelector: "*" } : {},
|
|
631
|
+
checkoutEveryNms: CHECKOUT_EVERY_MS
|
|
632
|
+
});
|
|
633
|
+
const flushTimer = setInterval(() => void flush(), FLUSH_INTERVAL_MS);
|
|
634
|
+
return {
|
|
635
|
+
stop: () => {
|
|
636
|
+
clearInterval(flushTimer);
|
|
637
|
+
stop?.();
|
|
638
|
+
},
|
|
639
|
+
flush,
|
|
640
|
+
getClickCount: () => clickCount
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region ../browser-session/src/replay-session.ts
|
|
645
|
+
/**
|
|
646
|
+
* Start recording the current browser session. Publishes the session sink,
|
|
647
|
+
* posts an `active` metadata row, and installs visibility handlers:
|
|
648
|
+
* hidden → flush + `ended` row (with observed trace ids) + stop capture;
|
|
649
|
+
* visible → re-resolve the session (rotating if idle-expired), republish the
|
|
650
|
+
* sink, restart capture, post a fresh `active` row. Metadata versions are
|
|
651
|
+
* monotonic per session, so the latest row always wins on the backend.
|
|
652
|
+
*
|
|
653
|
+
* Returns undefined outside a browser. Sampling is the caller's decision.
|
|
654
|
+
*/
|
|
655
|
+
function startReplaySession(options) {
|
|
656
|
+
if (typeof window === "undefined") return void 0;
|
|
657
|
+
const engineConfig = {
|
|
658
|
+
endpoint: options.endpoint.replace(/\/$/, ""),
|
|
659
|
+
ingestKey: options.ingestKey,
|
|
660
|
+
maskAllInputs: options.maskAllInputs,
|
|
661
|
+
maskAllText: options.maskAllText
|
|
662
|
+
};
|
|
663
|
+
const session = getSession();
|
|
664
|
+
let currentSessionId = session.id;
|
|
665
|
+
let currentStartedAt = new Date(session.startedAt);
|
|
666
|
+
publishSessionSink(currentSessionId);
|
|
667
|
+
let recorder;
|
|
668
|
+
let events;
|
|
669
|
+
let stopped = false;
|
|
670
|
+
const postMeta = (status, clickCount, keepalive = false) => postSessionMeta(engineConfig, buildSessionMetaRow({
|
|
671
|
+
sessionId: currentSessionId,
|
|
672
|
+
startedAt: currentStartedAt,
|
|
673
|
+
version: nextMetaVersion(),
|
|
674
|
+
status,
|
|
675
|
+
serviceName: options.serviceName,
|
|
676
|
+
userId: options.getUserId?.(),
|
|
677
|
+
environment: options.environment,
|
|
678
|
+
serviceVersion: options.serviceVersion,
|
|
679
|
+
clickCount: clickCount ?? 0,
|
|
680
|
+
traceIds: status === "ended" ? getObservedTraceIds() : void 0
|
|
681
|
+
}), keepalive);
|
|
682
|
+
const start = () => {
|
|
683
|
+
recorder = startRecording(engineConfig, currentSessionId);
|
|
684
|
+
events = startEventCapture(engineConfig, currentSessionId);
|
|
685
|
+
postMeta("active", null);
|
|
686
|
+
};
|
|
687
|
+
const suspend = () => {
|
|
688
|
+
if (!recorder || !events) return;
|
|
689
|
+
recorder.flush(true);
|
|
690
|
+
events.flush(true);
|
|
691
|
+
postMeta("ended", recorder.getClickCount(), true);
|
|
692
|
+
recorder.stop();
|
|
693
|
+
events.stop();
|
|
694
|
+
recorder = void 0;
|
|
695
|
+
events = void 0;
|
|
696
|
+
};
|
|
697
|
+
const resume = () => {
|
|
698
|
+
if (stopped || recorder) return;
|
|
699
|
+
const next = getSession();
|
|
700
|
+
if (next.id !== currentSessionId) {
|
|
701
|
+
currentSessionId = next.id;
|
|
702
|
+
currentStartedAt = new Date(next.startedAt);
|
|
703
|
+
publishSessionSink(currentSessionId);
|
|
704
|
+
}
|
|
705
|
+
start();
|
|
706
|
+
};
|
|
707
|
+
const onVisibilityChange = () => {
|
|
708
|
+
if (document.visibilityState === "hidden") suspend();
|
|
709
|
+
else resume();
|
|
710
|
+
};
|
|
711
|
+
const onPageHide = () => suspend();
|
|
712
|
+
start();
|
|
713
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
714
|
+
window.addEventListener("pagehide", onPageHide);
|
|
715
|
+
return {
|
|
716
|
+
sessionId: currentSessionId,
|
|
717
|
+
shutdown: async () => {
|
|
718
|
+
stopped = true;
|
|
719
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
720
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
721
|
+
if (recorder) await recorder.flush(true);
|
|
722
|
+
if (events) await events.flush(true);
|
|
723
|
+
recorder?.stop();
|
|
724
|
+
events?.stop();
|
|
725
|
+
recorder = void 0;
|
|
726
|
+
events = void 0;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/config.ts
|
|
732
|
+
const DEFAULT_ENDPOINT = "https://ingest.maple.dev";
|
|
733
|
+
function normalizeUserId(userId) {
|
|
734
|
+
return userId ? userId : void 0;
|
|
735
|
+
}
|
|
736
|
+
function resolveConfig(config) {
|
|
737
|
+
return {
|
|
738
|
+
ingestKey: config.ingestKey,
|
|
739
|
+
serviceName: config.serviceName,
|
|
740
|
+
endpoint: (config.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
741
|
+
serviceNamespace: config.serviceNamespace,
|
|
742
|
+
serviceVersion: config.serviceVersion,
|
|
743
|
+
environment: config.environment,
|
|
744
|
+
userId: normalizeUserId(config.userId),
|
|
745
|
+
tracingEnabled: config.tracing?.enabled ?? true,
|
|
746
|
+
tracingInstrumentFetch: config.tracing?.instrumentFetch ?? true,
|
|
747
|
+
replayEnabled: config.replay?.enabled ?? true,
|
|
748
|
+
replaySampleRate: config.replay?.sampleRate ?? 1,
|
|
749
|
+
maskAllInputs: config.privacy?.maskAllInputs ?? true,
|
|
750
|
+
maskAllText: config.privacy?.maskAllText ?? false
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/tracing.ts
|
|
755
|
+
/**
|
|
756
|
+
* Captures every span's trace id into the session sink. Lightweight — runs
|
|
757
|
+
* alongside the BatchSpanProcessor, does no export of its own.
|
|
758
|
+
*/
|
|
759
|
+
var TraceIdCollector = class {
|
|
760
|
+
constructor(getUserId = () => void 0) {
|
|
761
|
+
this.getUserId = getUserId;
|
|
762
|
+
}
|
|
763
|
+
onStart(span) {
|
|
764
|
+
recordTraceId(span.spanContext().traceId);
|
|
765
|
+
const userId = this.getUserId();
|
|
766
|
+
if (userId !== void 0) span.setAttribute("user.id", userId);
|
|
767
|
+
}
|
|
768
|
+
onEnd(_span) {}
|
|
769
|
+
forceFlush() {
|
|
770
|
+
return Promise.resolve();
|
|
771
|
+
}
|
|
772
|
+
shutdown() {
|
|
773
|
+
return Promise.resolve();
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
/**
|
|
777
|
+
* Set up browser OTel tracing exporting to Maple's ingest, tagging the resource
|
|
778
|
+
* with the shared `session.id`. When `tracingInstrumentFetch` is true, fetch()
|
|
779
|
+
* calls are auto-instrumented and their trace ids feed the session. Disable it
|
|
780
|
+
* when an external tracer (e.g. the Effect client SDK) already instruments
|
|
781
|
+
* requests — that tracer feeds the session via the published sink instead, and
|
|
782
|
+
* this avoids redundant duplicate network spans. Returns a shutdown function.
|
|
783
|
+
*/
|
|
784
|
+
function setupTracing(config, sessionId) {
|
|
785
|
+
const attributes = {
|
|
786
|
+
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
787
|
+
"maple.sdk.type": "browser",
|
|
788
|
+
"session.id": sessionId
|
|
789
|
+
};
|
|
790
|
+
if (config.serviceNamespace) attributes["service.namespace"] = config.serviceNamespace;
|
|
791
|
+
if (config.serviceVersion) {
|
|
792
|
+
attributes[ATTR_SERVICE_VERSION] = config.serviceVersion;
|
|
793
|
+
attributes["deployment.commit_sha"] = config.serviceVersion;
|
|
794
|
+
}
|
|
795
|
+
if (config.environment) {
|
|
796
|
+
attributes["deployment.environment"] = config.environment;
|
|
797
|
+
attributes["deployment.environment.name"] = config.environment;
|
|
798
|
+
}
|
|
799
|
+
const exporter = new OTLPTraceExporter({
|
|
800
|
+
url: `${config.endpoint}/v1/traces`,
|
|
801
|
+
headers: { Authorization: `Bearer ${config.ingestKey}` }
|
|
802
|
+
});
|
|
803
|
+
const provider = new WebTracerProvider({
|
|
804
|
+
resource: resourceFromAttributes(attributes),
|
|
805
|
+
spanProcessors: [new TraceIdCollector(() => config.userId), new BatchSpanProcessor(exporter)]
|
|
806
|
+
});
|
|
807
|
+
provider.register();
|
|
808
|
+
if (config.tracingInstrumentFetch) registerInstrumentations({ instrumentations: [new FetchInstrumentation({ ignoreUrls: [new RegExp(`${escapeRegExp(config.endpoint)}/v1/`)] })] });
|
|
809
|
+
return () => provider.shutdown();
|
|
810
|
+
}
|
|
811
|
+
function escapeRegExp(value) {
|
|
812
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
813
|
+
}
|
|
814
|
+
//#endregion
|
|
652
815
|
//#region src/init.ts
|
|
653
816
|
let active;
|
|
654
817
|
let activeConfig;
|
|
@@ -656,6 +819,10 @@ let activeConfig;
|
|
|
656
819
|
* Initialize Maple browser telemetry: OTel tracing + (sampled) rrweb session
|
|
657
820
|
* replay, both tagged with one shared session id so a trace can link to its
|
|
658
821
|
* replay and vice versa. Idempotent — repeated calls return the live handle.
|
|
822
|
+
*
|
|
823
|
+
* The replay lifecycle (suspend on tab-hidden, resume on visible, session
|
|
824
|
+
* metadata rows) lives in `@maple/browser-session` and is shared with the
|
|
825
|
+
* Effect client SDK's `replay` option.
|
|
659
826
|
*/
|
|
660
827
|
function init(rawConfig) {
|
|
661
828
|
if (active) return active;
|
|
@@ -666,26 +833,26 @@ function init(rawConfig) {
|
|
|
666
833
|
const config = resolveConfig(rawConfig);
|
|
667
834
|
activeConfig = config;
|
|
668
835
|
const session = getSession();
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const shutdownTracing = config.tracingEnabled ? setupTracing(config, sessionId) : void 0;
|
|
836
|
+
publishSessionSink(session.id);
|
|
837
|
+
setActiveTraceIdProvider(() => trace.getActiveSpan()?.spanContext().traceId);
|
|
838
|
+
const shutdownTracing = config.tracingEnabled ? setupTracing(config, session.id) : void 0;
|
|
673
839
|
const recordReplay = config.replayEnabled && Math.random() < config.replaySampleRate;
|
|
674
|
-
let
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
840
|
+
let replay;
|
|
841
|
+
if (recordReplay) replay = startReplaySession({
|
|
842
|
+
endpoint: config.endpoint,
|
|
843
|
+
ingestKey: config.ingestKey,
|
|
844
|
+
serviceName: config.serviceName,
|
|
845
|
+
environment: config.environment,
|
|
846
|
+
serviceVersion: config.serviceVersion,
|
|
847
|
+
maskAllInputs: config.maskAllInputs,
|
|
848
|
+
maskAllText: config.maskAllText,
|
|
849
|
+
getUserId: () => activeConfig?.userId
|
|
850
|
+
});
|
|
682
851
|
const handle = {
|
|
683
|
-
sessionId,
|
|
852
|
+
sessionId: session.id,
|
|
684
853
|
shutdown: async () => {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
recorder?.stop();
|
|
688
|
-
events?.stop();
|
|
854
|
+
await replay?.shutdown();
|
|
855
|
+
replay = void 0;
|
|
689
856
|
await shutdownTracing?.();
|
|
690
857
|
active = void 0;
|
|
691
858
|
activeConfig = void 0;
|
|
@@ -695,64 +862,14 @@ function init(rawConfig) {
|
|
|
695
862
|
return handle;
|
|
696
863
|
}
|
|
697
864
|
/**
|
|
698
|
-
* Attach
|
|
699
|
-
* call on every render.
|
|
700
|
-
*
|
|
701
|
-
* an id set here before the session ends is what the session is tagged with.
|
|
702
|
-
* We deliberately do not re-post the active row (the SDK already writes a fresh
|
|
703
|
-
* `Version=1` row on every reload; a second write here would collide under
|
|
704
|
-
* `argMax(field, Version)`).
|
|
865
|
+
* Attach, replace, or clear the user id on the active session. Idempotent and
|
|
866
|
+
* safe to call on every render. Future browser-created spans read this value
|
|
867
|
+
* when they start, and future session metadata rows read it when they post.
|
|
705
868
|
*/
|
|
706
869
|
function identify(userId) {
|
|
707
870
|
if (typeof window === "undefined") return;
|
|
708
871
|
if (!activeConfig) return;
|
|
709
|
-
|
|
710
|
-
if (activeConfig.userId === userId) return;
|
|
711
|
-
activeConfig.userId = userId;
|
|
712
|
-
}
|
|
713
|
-
function installLifecycleHandlers(config, sessionId, startedAt, recorder, events) {
|
|
714
|
-
let finalized = false;
|
|
715
|
-
const finalize = () => {
|
|
716
|
-
if (finalized) return;
|
|
717
|
-
finalized = true;
|
|
718
|
-
recorder.flush(true);
|
|
719
|
-
events.flush(true);
|
|
720
|
-
postSessionMeta(config, sessionMetaRow(config, sessionId, startedAt, 2, "ended", recorder.getClickCount()), true);
|
|
721
|
-
recorder.stop();
|
|
722
|
-
events.stop();
|
|
723
|
-
};
|
|
724
|
-
document.addEventListener("visibilitychange", () => {
|
|
725
|
-
if (document.visibilityState === "hidden") finalize();
|
|
726
|
-
});
|
|
727
|
-
window.addEventListener("pagehide", finalize);
|
|
728
|
-
}
|
|
729
|
-
function sessionMetaRow(config, sessionId, startedAt, version, status, clickCount) {
|
|
730
|
-
const ua = parseUserAgent(navigator.userAgent);
|
|
731
|
-
const now = /* @__PURE__ */ new Date();
|
|
732
|
-
const row = {
|
|
733
|
-
session_id: sessionId,
|
|
734
|
-
start_time: formatCHDateTime(startedAt),
|
|
735
|
-
status,
|
|
736
|
-
version,
|
|
737
|
-
user_id: config.userId ?? "",
|
|
738
|
-
url_initial: window.location.href,
|
|
739
|
-
user_agent: navigator.userAgent,
|
|
740
|
-
browser_name: ua.browserName,
|
|
741
|
-
os_name: ua.osName,
|
|
742
|
-
device_type: ua.deviceType,
|
|
743
|
-
service_name: config.serviceName,
|
|
744
|
-
resource_attributes: config.environment ? {
|
|
745
|
-
"deployment.environment": config.environment,
|
|
746
|
-
"deployment.environment.name": config.environment
|
|
747
|
-
} : {}
|
|
748
|
-
};
|
|
749
|
-
if (status === "ended") {
|
|
750
|
-
row.end_time = formatCHDateTime(now);
|
|
751
|
-
row.duration_ms = Math.max(0, now.getTime() - startedAt.getTime());
|
|
752
|
-
row.click_count = clickCount ?? 0;
|
|
753
|
-
row.trace_ids = getObservedTraceIds();
|
|
754
|
-
}
|
|
755
|
-
return row;
|
|
872
|
+
activeConfig.userId = normalizeUserId(userId);
|
|
756
873
|
}
|
|
757
874
|
//#endregion
|
|
758
875
|
//#region src/index.ts
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maple-dev/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Maple browser SDK — OpenTelemetry tracing and rrweb session replay in one package. Every span and replay event shares a session id for trace↔replay correlation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"maple",
|
|
7
|
+
"observability",
|
|
8
|
+
"opentelemetry",
|
|
9
|
+
"rrweb",
|
|
10
|
+
"rum",
|
|
11
|
+
"session-replay"
|
|
12
|
+
],
|
|
5
13
|
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
14
|
"files": [
|
|
8
15
|
"dist",
|
|
9
16
|
"README.md"
|
|
10
17
|
],
|
|
18
|
+
"type": "module",
|
|
11
19
|
"exports": {
|
|
12
20
|
".": {
|
|
13
21
|
"types": "./dist/index.d.mts",
|
|
@@ -31,16 +39,9 @@
|
|
|
31
39
|
"rrweb": "^2.0.0-alpha.18"
|
|
32
40
|
},
|
|
33
41
|
"devDependencies": {
|
|
42
|
+
"@maple/browser-session": "0.1.0",
|
|
34
43
|
"tsdown": "^0.21.7",
|
|
35
44
|
"typescript": "^6.0.2",
|
|
36
|
-
"vitest": "^4.1.
|
|
37
|
-
}
|
|
38
|
-
"keywords": [
|
|
39
|
-
"maple",
|
|
40
|
-
"opentelemetry",
|
|
41
|
-
"session-replay",
|
|
42
|
-
"rrweb",
|
|
43
|
-
"observability",
|
|
44
|
-
"rum"
|
|
45
|
-
]
|
|
45
|
+
"vitest": "^4.1.9"
|
|
46
|
+
}
|
|
46
47
|
}
|