@interfere/react 10.0.0 → 10.0.1-canary.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/api.d.mts.map +1 -1
- package/dist/api.mjs +1 -68
- package/dist/api.mjs.map +1 -1
- package/dist/error-boundary.d.mts +1 -2
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +1 -42
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/browser-context.d.mts.map +1 -1
- package/dist/internal/browser-context.mjs +1 -59
- package/dist/internal/browser-context.mjs.map +1 -1
- package/dist/internal/capture-boundary.d.mts +1 -2
- package/dist/internal/capture-boundary.d.mts.map +1 -1
- package/dist/internal/capture-boundary.mjs +1 -48
- package/dist/internal/capture-boundary.mjs.map +1 -1
- package/dist/internal/capture.mjs +1 -27
- package/dist/internal/capture.mjs.map +1 -1
- package/dist/internal/config.d.mts +3 -1
- package/dist/internal/config.d.mts.map +1 -1
- package/dist/internal/config.mjs +1 -33
- package/dist/internal/config.mjs.map +1 -1
- package/dist/internal/consent.d.mts.map +1 -1
- package/dist/internal/consent.mjs +1 -27
- package/dist/internal/consent.mjs.map +1 -1
- package/dist/internal/console-patch.d.mts.map +1 -1
- package/dist/internal/console-patch.mjs +1 -62
- package/dist/internal/console-patch.mjs.map +1 -1
- package/dist/internal/dom/actionable.d.mts.map +1 -1
- package/dist/internal/dom/actionable.mjs +1 -62
- package/dist/internal/dom/actionable.mjs.map +1 -1
- package/dist/internal/kernel-registry.d.mts.map +1 -1
- package/dist/internal/kernel-registry.mjs +1 -31
- package/dist/internal/kernel-registry.mjs.map +1 -1
- package/dist/internal/kernel.d.mts.map +1 -1
- package/dist/internal/kernel.mjs +1 -322
- package/dist/internal/kernel.mjs.map +1 -1
- package/dist/internal/otel/exporter.d.mts +4 -12
- package/dist/internal/otel/exporter.d.mts.map +1 -1
- package/dist/internal/otel/exporter.mjs +1 -212
- package/dist/internal/otel/exporter.mjs.map +1 -1
- package/dist/internal/otel/index.mjs +1 -6
- package/dist/internal/otel/instrumentations.d.mts.map +1 -1
- package/dist/internal/otel/instrumentations.mjs +1 -150
- package/dist/internal/otel/instrumentations.mjs.map +1 -1
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -1
- package/dist/internal/otel/page-scope-context-manager.mjs +1 -36
- package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -1
- package/dist/internal/otel/propagation.d.mts.map +1 -1
- package/dist/internal/otel/propagation.mjs +1 -40
- package/dist/internal/otel/propagation.mjs.map +1 -1
- package/dist/internal/otel/provider.d.mts +1 -2
- package/dist/internal/otel/provider.d.mts.map +1 -1
- package/dist/internal/otel/provider.mjs +1 -151
- package/dist/internal/otel/provider.mjs.map +1 -1
- package/dist/internal/otel/web-vitals.d.mts.map +1 -1
- package/dist/internal/otel/web-vitals.mjs +1 -162
- package/dist/internal/otel/web-vitals.mjs.map +1 -1
- package/dist/internal/page-lifecycle.d.mts.map +1 -1
- package/dist/internal/page-lifecycle.mjs +1 -33
- package/dist/internal/page-lifecycle.mjs.map +1 -1
- package/dist/internal/plugin-runtime.mjs +1 -101
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/internal/react-context.d.mts +1 -2
- package/dist/internal/react-context.d.mts.map +1 -1
- package/dist/internal/react-context.mjs +1 -34
- package/dist/internal/react-context.mjs.map +1 -1
- package/dist/internal/sw.d.mts.map +1 -1
- package/dist/internal/sw.mjs +1 -37
- package/dist/internal/sw.mjs.map +1 -1
- package/dist/internal/version.mjs +1 -7
- package/dist/internal/version.mjs.map +1 -1
- package/dist/internal/wrapper-singleton.d.mts.map +1 -1
- package/dist/internal/wrapper-singleton.mjs +1 -73
- package/dist/internal/wrapper-singleton.mjs.map +1 -1
- package/dist/package.mjs +1 -5
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +1 -84
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.mjs +1 -34
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/lib/types.d.mts.map +1 -1
- package/dist/plugins/lib/types.mjs +1 -1
- package/dist/plugins/logs.d.mts.map +1 -1
- package/dist/plugins/logs.mjs +1 -53
- package/dist/plugins/logs.mjs.map +1 -1
- package/dist/plugins/rage-clicks.d.mts.map +1 -1
- package/dist/plugins/rage-clicks.mjs +1 -55
- package/dist/plugins/rage-clicks.mjs.map +1 -1
- package/dist/plugins/replay.d.mts.map +1 -1
- package/dist/plugins/replay.mjs +1 -101
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +1 -31
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts.map +1 -1
- package/dist/react-error-handler.mjs +1 -62
- package/dist/react-error-handler.mjs.map +1 -1
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +1 -152
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/device.d.mts.map +1 -1
- package/dist/tracking/device.mjs +1 -104
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/tracking/geo.d.mts.map +1 -1
- package/dist/tracking/geo.mjs +2 -48
- package/dist/tracking/geo.mjs.map +1 -1
- package/dist/tracking/session.d.mts.map +1 -1
- package/dist/tracking/session.mjs +1 -75
- package/dist/tracking/session.mjs.map +1 -1
- package/dist/util/bot.d.mts.map +1 -1
- package/dist/util/bot.mjs +1 -14
- package/dist/util/bot.mjs.map +1 -1
- package/dist/util/global.d.mts.map +1 -1
- package/dist/util/global.mjs +1 -12
- package/dist/util/global.mjs.map +1 -1
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +1 -44
- package/dist/util/log.mjs.map +1 -1
- package/dist/util/stringify.d.mts.map +1 -1
- package/dist/util/stringify.mjs +1 -16
- package/dist/util/stringify.mjs.map +1 -1
- package/package.json +34 -33
|
@@ -1,73 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createKernel, isEnabledByEnvironment } from "./kernel.mjs";
|
|
3
|
-
import { isBotUserAgent } from "../util/bot.mjs";
|
|
4
|
-
//#region src/internal/wrapper-singleton.ts
|
|
5
|
-
/**
|
|
6
|
-
* Per-wrapper kernel-singleton lifecycle. The next/vite/etc wrappers used
|
|
7
|
-
* to copy the same ~100 lines verbatim (init/close/getKernel/subscribe
|
|
8
|
-
* /consent/identity); this is the single source of truth so a fix once
|
|
9
|
-
* applies everywhere.
|
|
10
|
-
*
|
|
11
|
-
* Each wrapper holds its own `WrapperSingleton` in module scope so the
|
|
12
|
-
* kernel is per-wrapper, not per-process. (Microfrontends can run two
|
|
13
|
-
* different wrappers on the same page; each gets its own kernel.)
|
|
14
|
-
*/
|
|
15
|
-
function createWrapperSingleton(input) {
|
|
16
|
-
let kernel = null;
|
|
17
|
-
let pending = null;
|
|
18
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
19
|
-
function emit() {
|
|
20
|
-
for (const listener of listeners) listener();
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
init(opts = {}) {
|
|
24
|
-
if (kernel) return Promise.resolve(kernel);
|
|
25
|
-
if (pending) return pending;
|
|
26
|
-
if (!(opts.enabled ?? isEnabledByEnvironment())) return Promise.resolve(null);
|
|
27
|
-
if (isBotUserAgent()) return Promise.resolve(null);
|
|
28
|
-
pending = createKernel({ opts: {
|
|
29
|
-
...opts,
|
|
30
|
-
_wrapperVersions: [input.producerVersion]
|
|
31
|
-
} }).then((k) => {
|
|
32
|
-
kernel = k;
|
|
33
|
-
if (k) registerKernel(k);
|
|
34
|
-
emit();
|
|
35
|
-
return k;
|
|
36
|
-
}).finally(() => {
|
|
37
|
-
pending = null;
|
|
38
|
-
});
|
|
39
|
-
return pending;
|
|
40
|
-
},
|
|
41
|
-
getKernel() {
|
|
42
|
-
if (!kernel) throw new Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);
|
|
43
|
-
return kernel;
|
|
44
|
-
},
|
|
45
|
-
getKernelOrNull() {
|
|
46
|
-
return kernel;
|
|
47
|
-
},
|
|
48
|
-
async close() {
|
|
49
|
-
if (!kernel) return;
|
|
50
|
-
const previous = kernel;
|
|
51
|
-
await previous.dispose();
|
|
52
|
-
kernel = null;
|
|
53
|
-
unregisterKernel(previous);
|
|
54
|
-
emit();
|
|
55
|
-
},
|
|
56
|
-
subscribeToKernel(listener) {
|
|
57
|
-
listeners.add(listener);
|
|
58
|
-
return () => {
|
|
59
|
-
listeners.delete(listener);
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
consent: {
|
|
63
|
-
get: () => kernel?.consent.get() ?? null,
|
|
64
|
-
set: (value) => kernel?.consent.set(value)
|
|
65
|
-
},
|
|
66
|
-
identity: {
|
|
67
|
-
get: () => kernel?.identity.get() ?? null,
|
|
68
|
-
set: (params) => kernel?.identity.set(params) ?? Promise.resolve()
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
//#endregion
|
|
73
|
-
export { createWrapperSingleton };
|
|
1
|
+
import{registerKernel,unregisterKernel}from"./kernel-registry.mjs";import{createKernel,isEnabledByEnvironment}from"./kernel.mjs";import{isBotUserAgent}from"../util/bot.mjs";function createWrapperSingleton(input){let kernel=null,pending=null,listeners=new Set;function emit(){for(let listener of listeners)listener()}return{init(opts={}){return kernel?Promise.resolve(kernel):pending||(!(opts.enabled??isEnabledByEnvironment())||isBotUserAgent()?Promise.resolve(null):(pending=createKernel({opts:{...opts,_wrapperVersions:[input.producerVersion]}}).then(k=>(kernel=k,k&®isterKernel(k),emit(),k)).finally(()=>{pending=null}),pending))},getKernel(){if(!kernel)throw Error(`Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`);return kernel},getKernelOrNull(){return kernel},async close(){if(!kernel)return;let previous=kernel;await previous.dispose(),kernel=null,unregisterKernel(previous),emit()},subscribeToKernel(listener){return listeners.add(listener),()=>{listeners.delete(listener)}},consent:{get:()=>kernel?.consent.get()??null,set:value=>kernel?.consent.set(value)},identity:{get:()=>kernel?.identity.get()??null,set:params=>kernel?.identity.set(params)??Promise.resolve()}}}export{createWrapperSingleton};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper-singleton.mjs","names":[],"sources":["../../src/internal/wrapper-singleton.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { isBotUserAgent } from \"../util/bot.js\";\nimport {\n createKernel,\n isEnabledByEnvironment,\n type Kernel,\n type KernelInternalOptions,\n type KernelOptions,\n} from \"./kernel.js\";\nimport { registerKernel, unregisterKernel } from \"./kernel-registry.js\";\n\nexport interface WrapperSingleton {\n close(): Promise<void>;\n consent: {\n get(): ConsentState | null;\n set(value?: ConsentState): void;\n };\n getKernel(): Kernel;\n getKernelOrNull(): Kernel | null;\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): Promise<void>;\n };\n init(opts?: KernelOptions): Promise<Kernel | null>;\n subscribeToKernel(listener: () => void): () => void;\n}\n\nexport interface WrapperSingletonInput {\n /**\n * Surfaced in the \"kernel not initialized\" error so the message points\n * customers at the right entry file (e.g. \"instrumentation-client\" /\n * \"main.ts\"). Single concrete fact — no other behavior change.\n */\n initEntryName: string;\n /**\n * Wrapper SDK identifier injected as the first entry in\n * `__INTERFERE_SDK_STACK__` (e.g. `@interfere/next@10.0.0`). Threaded\n * through to the kernel so the OTel resource carries the full stack.\n */\n producerVersion: string;\n}\n\n/**\n * Per-wrapper kernel-singleton lifecycle. The next/vite/etc wrappers used\n * to copy the same ~100 lines verbatim (init/close/getKernel/subscribe\n * /consent/identity); this is the single source of truth so a fix once\n * applies everywhere.\n *\n * Each wrapper holds its own `WrapperSingleton` in module scope so the\n * kernel is per-wrapper, not per-process. (Microfrontends can run two\n * different wrappers on the same page; each gets its own kernel.)\n */\nexport function createWrapperSingleton(\n input: WrapperSingletonInput\n): WrapperSingleton {\n let kernel: Kernel | null = null;\n let pending: Promise<Kernel | null> | null = null;\n const listeners = new Set<() => void>();\n\n function emit(): void {\n for (const listener of listeners) {\n listener();\n }\n }\n\n return {\n init(opts: KernelOptions = {}): Promise<Kernel | null> {\n if (kernel) {\n return Promise.resolve(kernel);\n }\n if (pending) {\n return pending;\n }\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n return Promise.resolve(null);\n }\n if (isBotUserAgent()) {\n return Promise.resolve(null);\n }\n\n const internalOpts: KernelInternalOptions = {\n ...opts,\n _wrapperVersions: [input.producerVersion],\n };\n pending = createKernel({ opts: internalOpts })\n .then((k) => {\n kernel = k;\n if (k) {\n registerKernel(k);\n }\n emit();\n return k;\n })\n .finally(() => {\n pending = null;\n });\n\n return pending;\n },\n\n getKernel(): Kernel {\n if (!kernel) {\n throw new Error(\n `Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`\n );\n }\n return kernel;\n },\n\n getKernelOrNull(): Kernel | null {\n return kernel;\n },\n\n async close(): Promise<void> {\n if (!kernel) {\n return;\n }\n const previous = kernel;\n await previous.dispose();\n kernel = null;\n unregisterKernel(previous);\n emit();\n },\n\n subscribeToKernel(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n\n consent: {\n get: () => kernel?.consent.get() ?? null,\n set: (value) => kernel?.consent.set(value),\n },\n\n identity: {\n get: () => kernel?.identity.get() ?? null,\n set: (params) => kernel?.identity.set(params) ?? Promise.resolve(),\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"wrapper-singleton.mjs","names":[],"sources":["../../src/internal/wrapper-singleton.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\nimport type { ConsentState } from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { isBotUserAgent } from \"../util/bot.js\";\nimport {\n createKernel,\n isEnabledByEnvironment,\n type Kernel,\n type KernelInternalOptions,\n type KernelOptions,\n} from \"./kernel.js\";\nimport { registerKernel, unregisterKernel } from \"./kernel-registry.js\";\n\nexport interface WrapperSingleton {\n close(): Promise<void>;\n consent: {\n get(): ConsentState | null;\n set(value?: ConsentState): void;\n };\n getKernel(): Kernel;\n getKernelOrNull(): Kernel | null;\n identity: {\n get(): IdentifyParams | null;\n set(params: IdentifyParams): Promise<void>;\n };\n init(opts?: KernelOptions): Promise<Kernel | null>;\n subscribeToKernel(listener: () => void): () => void;\n}\n\nexport interface WrapperSingletonInput {\n /**\n * Surfaced in the \"kernel not initialized\" error so the message points\n * customers at the right entry file (e.g. \"instrumentation-client\" /\n * \"main.ts\"). Single concrete fact — no other behavior change.\n */\n initEntryName: string;\n /**\n * Wrapper SDK identifier injected as the first entry in\n * `__INTERFERE_SDK_STACK__` (e.g. `@interfere/next@10.0.0`). Threaded\n * through to the kernel so the OTel resource carries the full stack.\n */\n producerVersion: string;\n}\n\n/**\n * Per-wrapper kernel-singleton lifecycle. The next/vite/etc wrappers used\n * to copy the same ~100 lines verbatim (init/close/getKernel/subscribe\n * /consent/identity); this is the single source of truth so a fix once\n * applies everywhere.\n *\n * Each wrapper holds its own `WrapperSingleton` in module scope so the\n * kernel is per-wrapper, not per-process. (Microfrontends can run two\n * different wrappers on the same page; each gets its own kernel.)\n */\nexport function createWrapperSingleton(\n input: WrapperSingletonInput\n): WrapperSingleton {\n let kernel: Kernel | null = null;\n let pending: Promise<Kernel | null> | null = null;\n const listeners = new Set<() => void>();\n\n function emit(): void {\n for (const listener of listeners) {\n listener();\n }\n }\n\n return {\n init(opts: KernelOptions = {}): Promise<Kernel | null> {\n if (kernel) {\n return Promise.resolve(kernel);\n }\n if (pending) {\n return pending;\n }\n if (!(opts.enabled ?? isEnabledByEnvironment())) {\n return Promise.resolve(null);\n }\n if (isBotUserAgent()) {\n return Promise.resolve(null);\n }\n\n const internalOpts: KernelInternalOptions = {\n ...opts,\n _wrapperVersions: [input.producerVersion],\n };\n pending = createKernel({ opts: internalOpts })\n .then((k) => {\n kernel = k;\n if (k) {\n registerKernel(k);\n }\n emit();\n return k;\n })\n .finally(() => {\n pending = null;\n });\n\n return pending;\n },\n\n getKernel(): Kernel {\n if (!kernel) {\n throw new Error(\n `Interfere SDK not initialized. Call init() from your ${input.initEntryName} entrypoint.`\n );\n }\n return kernel;\n },\n\n getKernelOrNull(): Kernel | null {\n return kernel;\n },\n\n async close(): Promise<void> {\n if (!kernel) {\n return;\n }\n const previous = kernel;\n await previous.dispose();\n kernel = null;\n unregisterKernel(previous);\n emit();\n },\n\n subscribeToKernel(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n\n consent: {\n get: () => kernel?.consent.get() ?? null,\n set: (value) => kernel?.consent.set(value),\n },\n\n identity: {\n get: () => kernel?.identity.get() ?? null,\n set: (params) => kernel?.identity.set(params) ?? Promise.resolve(),\n },\n };\n}\n"],"mappings":"6KAsDA,SAAgB,uBACd,MACkB,CAClB,IAAI,OAAwB,KACxB,QAAyC,KACvC,UAAY,IAAI,IAEtB,SAAS,MAAa,CACpB,IAAK,IAAM,YAAY,UACrB,SAAS,CAEb,CAEA,MAAO,CACL,KAAK,KAAsB,CAAC,EAA2B,CA+BrD,OA9BI,OACK,QAAQ,QAAQ,MAAM,EAE3B,UAGA,EAAE,KAAK,SAAW,uBAAuB,IAGzC,eAAe,EACV,QAAQ,QAAQ,IAAI,GAO7B,QAAU,aAAa,CAAE,KAAM,CAH7B,GAAG,KACH,iBAAkB,CAAC,MAAM,eAAe,CAEA,CAAE,CAAC,EAC1C,KAAM,IACL,OAAS,EACL,GACF,eAAe,CAAC,EAElB,KAAK,EACE,EACR,EACA,YAAc,CACb,QAAU,IACZ,CAAC,EAEI,SACT,EAEA,WAAoB,CAClB,GAAI,CAAC,OACH,MAAU,MACR,wDAAwD,MAAM,cAAc,aAC9E,EAEF,OAAO,MACT,EAEA,iBAAiC,CAC/B,OAAO,MACT,EAEA,MAAM,OAAuB,CAC3B,GAAI,CAAC,OACH,OAEF,IAAM,SAAW,OACjB,MAAM,SAAS,QAAQ,EACvB,OAAS,KACT,iBAAiB,QAAQ,EACzB,KAAK,CACP,EAEA,kBAAkB,SAAkC,CAElD,OADA,UAAU,IAAI,QAAQ,MACT,CACX,UAAU,OAAO,QAAQ,CAC3B,CACF,EAEA,QAAS,CACP,QAAW,QAAQ,QAAQ,IAAI,GAAK,KACpC,IAAM,OAAU,QAAQ,QAAQ,IAAI,KAAK,CAC3C,EAEA,SAAU,CACR,QAAW,QAAQ,SAAS,IAAI,GAAK,KACrC,IAAM,QAAW,QAAQ,SAAS,IAAI,MAAM,GAAK,QAAQ,QAAQ,CACnE,CACF,CACF"}
|
package/dist/package.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/plugins/errors.ts"],"mappings":";;;cAwDa,YAAA,EAAc,
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/plugins/errors.ts"],"mappings":";;;cAwDa,YAAA,EAAc,MA0F1B"}
|
package/dist/plugins/errors.mjs
CHANGED
|
@@ -1,84 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { MECHANISM_TYPE, isNonErrorException, toException } from "@interfere/types/sdk/errors";
|
|
3
|
-
//#region src/plugins/errors.ts
|
|
4
|
-
/**
|
|
5
|
-
* V8's default stack limit is 10. Deep React trees routinely exceed that
|
|
6
|
-
* before reaching the actual application frames, leaving only react-dom
|
|
7
|
-
* internals in the captured stack. Matches Sentry's browser SDK.
|
|
8
|
-
*/
|
|
9
|
-
const STACK_TRACE_LIMIT = 50;
|
|
10
|
-
function capture(ctx, opts) {
|
|
11
|
-
ctx.recordException(opts.error, {
|
|
12
|
-
mechanism: opts.mechanism,
|
|
13
|
-
...opts.fallbackFrame && !isNonErrorException(opts.error) ? { fallbackFrames: [opts.fallbackFrame] } : {}
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Finds the first `Error` instance in a list of `console.error` arguments.
|
|
18
|
-
* React 18+ in production logs errors as
|
|
19
|
-
* `console.error("The above error occurred in ...", error, componentStack)`,
|
|
20
|
-
* so scanning only `args[0]` misses React's own uncaught-error reports.
|
|
21
|
-
*/
|
|
22
|
-
function findErrorArg(args) {
|
|
23
|
-
for (const arg of args) if (arg instanceof Error) return arg;
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
const errorsPlugin = {
|
|
27
|
-
name: "errors",
|
|
28
|
-
setup(ctx) {
|
|
29
|
-
const originalOnError = globalThis.onerror;
|
|
30
|
-
const originalStackTraceLimit = Error.stackTraceLimit;
|
|
31
|
-
if (Error.stackTraceLimit < STACK_TRACE_LIMIT) Error.stackTraceLimit = STACK_TRACE_LIMIT;
|
|
32
|
-
globalThis.onerror = (msg, source, line, col, error) => {
|
|
33
|
-
if (error instanceof Error) {
|
|
34
|
-
const fallbackFrame = typeof source === "string" ? {
|
|
35
|
-
id: "fallback-0",
|
|
36
|
-
file: source,
|
|
37
|
-
...typeof line === "number" ? { line } : {},
|
|
38
|
-
...typeof col === "number" ? { column: col } : {}
|
|
39
|
-
} : null;
|
|
40
|
-
capture(ctx, {
|
|
41
|
-
error,
|
|
42
|
-
mechanism: {
|
|
43
|
-
type: MECHANISM_TYPE.browser.onerror,
|
|
44
|
-
handled: false
|
|
45
|
-
},
|
|
46
|
-
...fallbackFrame ? { fallbackFrame } : {}
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
if (typeof originalOnError === "function") return originalOnError.call(globalThis, msg, source, line, col, error);
|
|
50
|
-
return false;
|
|
51
|
-
};
|
|
52
|
-
const onUnhandledRejection = (event) => {
|
|
53
|
-
capture(ctx, {
|
|
54
|
-
error: toException(event.reason),
|
|
55
|
-
mechanism: {
|
|
56
|
-
type: MECHANISM_TYPE.browser.onunhandledrejection,
|
|
57
|
-
handled: false,
|
|
58
|
-
...event.reason instanceof Error ? {} : { synthetic: true }
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
};
|
|
62
|
-
const supportsEventTarget = typeof globalThis.addEventListener === "function" && typeof globalThis.removeEventListener === "function";
|
|
63
|
-
if (supportsEventTarget) globalThis.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
64
|
-
const unsubscribeConsole = onConsoleCall((level, args) => {
|
|
65
|
-
if (level !== "error") return;
|
|
66
|
-
const error = findErrorArg(args);
|
|
67
|
-
if (error) capture(ctx, {
|
|
68
|
-
error,
|
|
69
|
-
mechanism: {
|
|
70
|
-
type: MECHANISM_TYPE.browser.consoleError,
|
|
71
|
-
handled: true
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
return () => {
|
|
76
|
-
Error.stackTraceLimit = originalStackTraceLimit;
|
|
77
|
-
globalThis.onerror = originalOnError;
|
|
78
|
-
if (supportsEventTarget) globalThis.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
79
|
-
unsubscribeConsole();
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
//#endregion
|
|
84
|
-
export { errorsPlugin as default, errorsPlugin };
|
|
1
|
+
import{onConsoleCall}from"../internal/console-patch.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";function capture(ctx,opts){ctx.recordException(opts.error,{mechanism:opts.mechanism,...opts.fallbackFrame&&!isNonErrorException(opts.error)?{fallbackFrames:[opts.fallbackFrame]}:{}})}function findErrorArg(args){for(let arg of args)if(arg instanceof Error)return arg;return null}const errorsPlugin={name:`errors`,setup(ctx){let originalOnError=globalThis.onerror,originalStackTraceLimit=Error.stackTraceLimit;Error.stackTraceLimit<50&&(Error.stackTraceLimit=50),globalThis.onerror=(msg,source,line,col,error)=>{if(error instanceof Error){let fallbackFrame=typeof source==`string`?{id:`fallback-0`,file:source,...typeof line==`number`?{line}:{},...typeof col==`number`?{column:col}:{}}:null;capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.onerror,handled:!1},...fallbackFrame?{fallbackFrame}:{}})}return typeof originalOnError==`function`?originalOnError.call(globalThis,msg,source,line,col,error):!1};let onUnhandledRejection=event=>{capture(ctx,{error:toException(event.reason),mechanism:{type:MECHANISM_TYPE.browser.onunhandledrejection,handled:!1,...event.reason instanceof Error?{}:{synthetic:!0}}})},supportsEventTarget=typeof globalThis.addEventListener==`function`&&typeof globalThis.removeEventListener==`function`;supportsEventTarget&&globalThis.addEventListener(`unhandledrejection`,onUnhandledRejection);let unsubscribeConsole=onConsoleCall((level,args)=>{if(level!==`error`)return;let error=findErrorArg(args);error&&capture(ctx,{error,mechanism:{type:MECHANISM_TYPE.browser.consoleError,handled:!0}})});return()=>{Error.stackTraceLimit=originalStackTraceLimit,globalThis.onerror=originalOnError,supportsEventTarget&&globalThis.removeEventListener(`unhandledrejection`,onUnhandledRejection),unsubscribeConsole()}}};export{errorsPlugin as default,errorsPlugin};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../src/plugins/errors.ts"],"sourcesContent":["import type { IngestedFrame } from \"@interfere/types/data/frame\";\nimport {\n isNonErrorException,\n MECHANISM_TYPE,\n type NonErrorException,\n toException,\n} from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { onConsoleCall } from \"../internal/console-patch.js\";\nimport type { Plugin, PluginContext } from \"./lib/types.js\";\n\n/**\n * V8's default stack limit is 10. Deep React trees routinely exceed that\n * before reaching the actual application frames, leaving only react-dom\n * internals in the captured stack. Matches Sentry's browser SDK.\n */\nconst STACK_TRACE_LIMIT = 50;\n\ninterface CaptureOpts {\n readonly error: Error | NonErrorException;\n /**\n * Fallback frame to inject when the parsed stack of the root exception is\n * empty. Used for `window.onerror` calls where the browser provides\n * `source`/`line`/`col` even though the Error object itself has a\n * degenerate stack. Ignored for non-Error captures — there's no stack\n * to fall back into.\n */\n readonly fallbackFrame?: IngestedFrame;\n readonly mechanism: ErrorMechanism;\n}\n\nfunction capture(ctx: PluginContext, opts: CaptureOpts) {\n ctx.recordException(opts.error, {\n mechanism: opts.mechanism,\n ...(opts.fallbackFrame && !isNonErrorException(opts.error)\n ? { fallbackFrames: [opts.fallbackFrame] }\n : {}),\n });\n}\n\n/**\n * Finds the first `Error` instance in a list of `console.error` arguments.\n * React 18+ in production logs errors as\n * `console.error(\"The above error occurred in ...\", error, componentStack)`,\n * so scanning only `args[0]` misses React's own uncaught-error reports.\n */\nfunction findErrorArg(args: readonly unknown[]): Error | null {\n for (const arg of args) {\n if (arg instanceof Error) {\n return arg;\n }\n }\n return null;\n}\n\nexport const errorsPlugin: Plugin = {\n name: \"errors\",\n\n setup(ctx) {\n const originalOnError = globalThis.onerror;\n const originalStackTraceLimit = Error.stackTraceLimit;\n if (Error.stackTraceLimit < STACK_TRACE_LIMIT) {\n Error.stackTraceLimit = STACK_TRACE_LIMIT;\n }\n\n globalThis.onerror = (msg, source, line, col, error) => {\n if (error instanceof Error) {\n const fallbackFrame =\n typeof source === \"string\"\n ? {\n id: \"fallback-0\",\n file: source,\n ...(typeof line === \"number\" ? { line } : {}),\n ...(typeof col === \"number\" ? { column: col } : {}),\n }\n : null;\n\n capture(ctx, {\n error,\n mechanism: { type: MECHANISM_TYPE.browser.onerror, handled: false },\n ...(fallbackFrame ? { fallbackFrame } : {}),\n });\n }\n if (typeof originalOnError === \"function\") {\n return originalOnError.call(globalThis, msg, source, line, col, error);\n }\n return false;\n };\n\n const onUnhandledRejection = (event: PromiseRejectionEvent) => {\n // `toException` returns a real Error when the rejection carries a\n // recoverable one (direct Error, nested Error inside an object, or\n // a plain string), or a `NonErrorException` carrying the original\n // structured payload otherwise. The `synthetic: true` flag was\n // historically a \"we lied about the shape\" tag; with structured\n // capture it now means \"this isn't a JS Error\" — same intent,\n // honest signal.\n capture(ctx, {\n error: toException(event.reason),\n mechanism: {\n type: MECHANISM_TYPE.browser.onunhandledrejection,\n handled: false,\n ...(event.reason instanceof Error ? {} : { synthetic: true }),\n },\n });\n };\n // Some non-browser runtimes (notably Node test forks vitest spawns under\n // `pool: \"forks\"`) don't expose `addEventListener` on globalThis. Skip the\n // unhandledrejection hook there rather than crashing plugin setup; the\n // browser path is unaffected.\n const supportsEventTarget =\n typeof globalThis.addEventListener === \"function\" &&\n typeof globalThis.removeEventListener === \"function\";\n if (supportsEventTarget) {\n globalThis.addEventListener(\"unhandledrejection\", onUnhandledRejection);\n }\n\n const unsubscribeConsole = onConsoleCall((level, args) => {\n if (level !== \"error\") {\n return;\n }\n const error = findErrorArg(args);\n if (error) {\n capture(ctx, {\n error,\n mechanism: {\n type: MECHANISM_TYPE.browser.consoleError,\n handled: true,\n },\n });\n }\n });\n\n return () => {\n Error.stackTraceLimit = originalStackTraceLimit;\n globalThis.onerror = originalOnError;\n if (supportsEventTarget) {\n globalThis.removeEventListener(\n \"unhandledrejection\",\n onUnhandledRejection\n );\n }\n unsubscribeConsole();\n };\n },\n};\n\nexport default errorsPlugin;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../src/plugins/errors.ts"],"sourcesContent":["import type { IngestedFrame } from \"@interfere/types/data/frame\";\nimport {\n isNonErrorException,\n MECHANISM_TYPE,\n type NonErrorException,\n toException,\n} from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { onConsoleCall } from \"../internal/console-patch.js\";\nimport type { Plugin, PluginContext } from \"./lib/types.js\";\n\n/**\n * V8's default stack limit is 10. Deep React trees routinely exceed that\n * before reaching the actual application frames, leaving only react-dom\n * internals in the captured stack. Matches Sentry's browser SDK.\n */\nconst STACK_TRACE_LIMIT = 50;\n\ninterface CaptureOpts {\n readonly error: Error | NonErrorException;\n /**\n * Fallback frame to inject when the parsed stack of the root exception is\n * empty. Used for `window.onerror` calls where the browser provides\n * `source`/`line`/`col` even though the Error object itself has a\n * degenerate stack. Ignored for non-Error captures — there's no stack\n * to fall back into.\n */\n readonly fallbackFrame?: IngestedFrame;\n readonly mechanism: ErrorMechanism;\n}\n\nfunction capture(ctx: PluginContext, opts: CaptureOpts) {\n ctx.recordException(opts.error, {\n mechanism: opts.mechanism,\n ...(opts.fallbackFrame && !isNonErrorException(opts.error)\n ? { fallbackFrames: [opts.fallbackFrame] }\n : {}),\n });\n}\n\n/**\n * Finds the first `Error` instance in a list of `console.error` arguments.\n * React 18+ in production logs errors as\n * `console.error(\"The above error occurred in ...\", error, componentStack)`,\n * so scanning only `args[0]` misses React's own uncaught-error reports.\n */\nfunction findErrorArg(args: readonly unknown[]): Error | null {\n for (const arg of args) {\n if (arg instanceof Error) {\n return arg;\n }\n }\n return null;\n}\n\nexport const errorsPlugin: Plugin = {\n name: \"errors\",\n\n setup(ctx) {\n const originalOnError = globalThis.onerror;\n const originalStackTraceLimit = Error.stackTraceLimit;\n if (Error.stackTraceLimit < STACK_TRACE_LIMIT) {\n Error.stackTraceLimit = STACK_TRACE_LIMIT;\n }\n\n globalThis.onerror = (msg, source, line, col, error) => {\n if (error instanceof Error) {\n const fallbackFrame =\n typeof source === \"string\"\n ? {\n id: \"fallback-0\",\n file: source,\n ...(typeof line === \"number\" ? { line } : {}),\n ...(typeof col === \"number\" ? { column: col } : {}),\n }\n : null;\n\n capture(ctx, {\n error,\n mechanism: { type: MECHANISM_TYPE.browser.onerror, handled: false },\n ...(fallbackFrame ? { fallbackFrame } : {}),\n });\n }\n if (typeof originalOnError === \"function\") {\n return originalOnError.call(globalThis, msg, source, line, col, error);\n }\n return false;\n };\n\n const onUnhandledRejection = (event: PromiseRejectionEvent) => {\n // `toException` returns a real Error when the rejection carries a\n // recoverable one (direct Error, nested Error inside an object, or\n // a plain string), or a `NonErrorException` carrying the original\n // structured payload otherwise. The `synthetic: true` flag was\n // historically a \"we lied about the shape\" tag; with structured\n // capture it now means \"this isn't a JS Error\" — same intent,\n // honest signal.\n capture(ctx, {\n error: toException(event.reason),\n mechanism: {\n type: MECHANISM_TYPE.browser.onunhandledrejection,\n handled: false,\n ...(event.reason instanceof Error ? {} : { synthetic: true }),\n },\n });\n };\n // Some non-browser runtimes (notably Node test forks vitest spawns under\n // `pool: \"forks\"`) don't expose `addEventListener` on globalThis. Skip the\n // unhandledrejection hook there rather than crashing plugin setup; the\n // browser path is unaffected.\n const supportsEventTarget =\n typeof globalThis.addEventListener === \"function\" &&\n typeof globalThis.removeEventListener === \"function\";\n if (supportsEventTarget) {\n globalThis.addEventListener(\"unhandledrejection\", onUnhandledRejection);\n }\n\n const unsubscribeConsole = onConsoleCall((level, args) => {\n if (level !== \"error\") {\n return;\n }\n const error = findErrorArg(args);\n if (error) {\n capture(ctx, {\n error,\n mechanism: {\n type: MECHANISM_TYPE.browser.consoleError,\n handled: true,\n },\n });\n }\n });\n\n return () => {\n Error.stackTraceLimit = originalStackTraceLimit;\n globalThis.onerror = originalOnError;\n if (supportsEventTarget) {\n globalThis.removeEventListener(\n \"unhandledrejection\",\n onUnhandledRejection\n );\n }\n unsubscribeConsole();\n };\n },\n};\n\nexport default errorsPlugin;\n"],"mappings":"iJAgCA,SAAS,QAAQ,IAAoB,KAAmB,CACtD,IAAI,gBAAgB,KAAK,MAAO,CAC9B,UAAW,KAAK,UAChB,GAAI,KAAK,eAAiB,CAAC,oBAAoB,KAAK,KAAK,EACrD,CAAE,eAAgB,CAAC,KAAK,aAAa,CAAE,EACvC,CAAC,CACP,CAAC,CACH,CAQA,SAAS,aAAa,KAAwC,CAC5D,IAAK,IAAM,OAAO,KAChB,GAAI,eAAe,MACjB,OAAO,IAGX,OAAO,IACT,CAEA,MAAa,aAAuB,CAClC,KAAM,SAEN,MAAM,IAAK,CACT,IAAM,gBAAkB,WAAW,QAC7B,wBAA0B,MAAM,gBAClC,MAAM,gBAAkB,KAC1B,MAAM,gBAAkB,IAG1B,WAAW,SAAW,IAAK,OAAQ,KAAM,IAAK,QAAU,CACtD,GAAI,iBAAiB,MAAO,CAC1B,IAAM,cACJ,OAAO,QAAW,SACd,CACE,GAAI,aACJ,KAAM,OACN,GAAI,OAAO,MAAS,SAAW,CAAE,IAAK,EAAI,CAAC,EAC3C,GAAI,OAAO,KAAQ,SAAW,CAAE,OAAQ,GAAI,EAAI,CAAC,CACnD,EACA,KAEN,QAAQ,IAAK,CACX,MACA,UAAW,CAAE,KAAM,eAAe,QAAQ,QAAS,QAAS,EAAM,EAClE,GAAI,cAAgB,CAAE,aAAc,EAAI,CAAC,CAC3C,CAAC,CACH,CAIA,OAHI,OAAO,iBAAoB,WACtB,gBAAgB,KAAK,WAAY,IAAK,OAAQ,KAAM,IAAK,KAAK,EAEhE,EACT,EAEA,IAAM,qBAAwB,OAAiC,CAQ7D,QAAQ,IAAK,CACX,MAAO,YAAY,MAAM,MAAM,EAC/B,UAAW,CACT,KAAM,eAAe,QAAQ,qBAC7B,QAAS,GACT,GAAI,MAAM,kBAAkB,MAAQ,CAAC,EAAI,CAAE,UAAW,EAAK,CAC7D,CACF,CAAC,CACH,EAKM,oBACJ,OAAO,WAAW,kBAAqB,YACvC,OAAO,WAAW,qBAAwB,WACxC,qBACF,WAAW,iBAAiB,qBAAsB,oBAAoB,EAGxE,IAAM,mBAAqB,eAAe,MAAO,OAAS,CACxD,GAAI,QAAU,QACZ,OAEF,IAAM,MAAQ,aAAa,IAAI,EAC3B,OACF,QAAQ,IAAK,CACX,MACA,UAAW,CACT,KAAM,eAAe,QAAQ,aAC7B,QAAS,EACX,CACF,CAAC,CAEL,CAAC,EAED,UAAa,CACX,MAAM,gBAAkB,wBACxB,WAAW,QAAU,gBACjB,qBACF,WAAW,oBACT,qBACA,oBACF,EAEF,mBAAmB,CACrB,CACF,CACF"}
|
|
@@ -1,34 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
|
|
3
|
-
//#region src/plugins/lib/loader.ts
|
|
4
|
-
const log = createLogger("plugins");
|
|
5
|
-
const LOADERS = {
|
|
6
|
-
errors: () => import("../errors.mjs"),
|
|
7
|
-
logs: () => import("../logs.mjs"),
|
|
8
|
-
rageClick: () => import("../rage-clicks.mjs"),
|
|
9
|
-
replay: () => import("../replay.mjs")
|
|
10
|
-
};
|
|
11
|
-
const DEFAULTS = Object.fromEntries(PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled]));
|
|
12
|
-
function resolveFeatures(overrides) {
|
|
13
|
-
return {
|
|
14
|
-
...DEFAULTS,
|
|
15
|
-
...overrides
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function resolvePlugin(mod) {
|
|
19
|
-
return "default" in mod && typeof mod.default.setup === "function" ? mod.default : mod;
|
|
20
|
-
}
|
|
21
|
-
async function loadPlugin(key, context) {
|
|
22
|
-
const loader = LOADERS[key];
|
|
23
|
-
if (!loader) return null;
|
|
24
|
-
try {
|
|
25
|
-
const cleanup = resolvePlugin(await loader()).setup(context);
|
|
26
|
-
log.debug("loaded %s", key);
|
|
27
|
-
return typeof cleanup === "function" ? cleanup : null;
|
|
28
|
-
} catch {
|
|
29
|
-
log.error("failed to load plugin %s", key);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
34
|
-
export { loadPlugin, resolveFeatures };
|
|
1
|
+
import{createLogger}from"../../util/log.mjs";import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const log=createLogger(`plugins`),LOADERS={errors:()=>import(`../errors.mjs`),logs:()=>import(`../logs.mjs`),rageClick:()=>import(`../rage-clicks.mjs`),replay:()=>import(`../replay.mjs`)},DEFAULTS=Object.fromEntries(PLUGIN_MANIFEST.map(p=>[p.name,p.defaultEnabled]));function resolveFeatures(overrides){return{...DEFAULTS,...overrides}}function resolvePlugin(mod){return`default`in mod&&typeof mod.default.setup==`function`?mod.default:mod}async function loadPlugin(key,context){let loader=LOADERS[key];if(!loader)return null;try{let cleanup=resolvePlugin(await loader()).setup(context);return log.debug(`loaded %s`,key),typeof cleanup==`function`?cleanup:null}catch{return log.error(`failed to load plugin %s`,key),null}}export{loadPlugin,resolveFeatures};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.mjs","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"sourcesContent":["import {\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { createLogger } from \"../../util/log.js\";\nimport type { Plugin, PluginCleanup, PluginContext } from \"./types.js\";\n\nconst log = createLogger(\"plugins\");\n\ntype PluginLoader = () => Promise<{ default: Plugin } | Plugin>;\n\n// `pageEvents` from PLUGIN_MANIFEST has no loader: pageviews / clicks /\n// pageleaves are covered by the OTel `BrowserNavigationInstrumentation` +\n// `UserInteractionInstrumentation` auto-instruments instead. The\n// manifest entry stays for the legacy `/v1/ingest` server-side validators\n// (old SDK 9.x clients still emit these envelopes).\nconst LOADERS: Partial<Record<PluginKey, PluginLoader>> = {\n errors: () => import(\"../errors.js\"),\n logs: () => import(\"../logs.js\"),\n rageClick: () => import(\"../rage-clicks.js\"),\n replay: () => import(\"../replay.js\"),\n};\n\nconst DEFAULTS: Record<PluginKey, boolean> = Object.fromEntries(\n PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled])\n) as Record<PluginKey, boolean>;\n\nexport type PluginOverrides = Partial<Record<PluginKey, boolean>>;\n\nexport function resolveFeatures(\n overrides?: PluginOverrides\n): Record<PluginKey, boolean> {\n return { ...DEFAULTS, ...overrides };\n}\n\nfunction resolvePlugin(mod: { default: Plugin } | Plugin): Plugin {\n return \"default\" in mod && typeof (mod.default as Plugin).setup === \"function\"\n ? mod.default\n : (mod as Plugin);\n}\n\nexport async function loadPlugin(\n key: PluginKey,\n context: PluginContext\n): Promise<PluginCleanup | null> {\n const loader = LOADERS[key];\n if (!loader) {\n return null;\n }\n\n try {\n const mod = await loader();\n const plugin = resolvePlugin(mod);\n const cleanup = plugin.setup(context);\n log.debug(\"loaded %s\", key);\n return typeof cleanup === \"function\" ? cleanup : null;\n } catch {\n log.error(\"failed to load plugin %s\", key);\n return null;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"loader.mjs","names":[],"sources":["../../../src/plugins/lib/loader.ts"],"sourcesContent":["import {\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\n\nimport { createLogger } from \"../../util/log.js\";\nimport type { Plugin, PluginCleanup, PluginContext } from \"./types.js\";\n\nconst log = createLogger(\"plugins\");\n\ntype PluginLoader = () => Promise<{ default: Plugin } | Plugin>;\n\n// `pageEvents` from PLUGIN_MANIFEST has no loader: pageviews / clicks /\n// pageleaves are covered by the OTel `BrowserNavigationInstrumentation` +\n// `UserInteractionInstrumentation` auto-instruments instead. The\n// manifest entry stays for the legacy `/v1/ingest` server-side validators\n// (old SDK 9.x clients still emit these envelopes).\nconst LOADERS: Partial<Record<PluginKey, PluginLoader>> = {\n errors: () => import(\"../errors.js\"),\n logs: () => import(\"../logs.js\"),\n rageClick: () => import(\"../rage-clicks.js\"),\n replay: () => import(\"../replay.js\"),\n};\n\nconst DEFAULTS: Record<PluginKey, boolean> = Object.fromEntries(\n PLUGIN_MANIFEST.map((p) => [p.name, p.defaultEnabled])\n) as Record<PluginKey, boolean>;\n\nexport type PluginOverrides = Partial<Record<PluginKey, boolean>>;\n\nexport function resolveFeatures(\n overrides?: PluginOverrides\n): Record<PluginKey, boolean> {\n return { ...DEFAULTS, ...overrides };\n}\n\nfunction resolvePlugin(mod: { default: Plugin } | Plugin): Plugin {\n return \"default\" in mod && typeof (mod.default as Plugin).setup === \"function\"\n ? mod.default\n : (mod as Plugin);\n}\n\nexport async function loadPlugin(\n key: PluginKey,\n context: PluginContext\n): Promise<PluginCleanup | null> {\n const loader = LOADERS[key];\n if (!loader) {\n return null;\n }\n\n try {\n const mod = await loader();\n const plugin = resolvePlugin(mod);\n const cleanup = plugin.setup(context);\n log.debug(\"loaded %s\", key);\n return typeof cleanup === \"function\" ? cleanup : null;\n } catch {\n log.error(\"failed to load plugin %s\", key);\n return null;\n }\n}\n"],"mappings":"gHAQA,MAAM,IAAM,aAAa,SAAS,EAS5B,QAAoD,CACxD,WAAc,OAAO,iBACrB,SAAY,OAAO,eACnB,cAAiB,OAAO,sBACxB,WAAc,OAAO,gBACvB,EAEM,SAAuC,OAAO,YAClD,gBAAgB,IAAK,GAAM,CAAC,EAAE,KAAM,EAAE,cAAc,CAAC,CACvD,EAIA,SAAgB,gBACd,UAC4B,CAC5B,MAAO,CAAE,GAAG,SAAU,GAAG,SAAU,CACrC,CAEA,SAAS,cAAc,IAA2C,CAChE,MAAO,YAAa,KAAO,OAAQ,IAAI,QAAmB,OAAU,WAChE,IAAI,QACH,GACP,CAEA,eAAsB,WACpB,IACA,QAC+B,CAC/B,IAAM,OAAS,QAAQ,KACvB,GAAI,CAAC,OACH,OAAO,KAGT,GAAI,CAGF,IAAM,QADS,cAAc,MADX,OAAO,CAEJ,EAAE,MAAM,OAAO,EAEpC,OADA,IAAI,MAAM,YAAa,GAAG,EACnB,OAAO,SAAY,WAAa,QAAU,IACnD,MAAQ,CAEN,OADA,IAAI,MAAM,2BAA4B,GAAG,EAClC,IACT,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../src/plugins/lib/types.ts"],"mappings":";;;;KAIY,aAAA;AAAA,UAEK,aAAA;EACf,YAAA;EACA,eAAA,CACE,KAAA,EAAO,KAAA,GAAQ,iBAAA,EACf,IAAA,EAAM,mBAAA;AAAA;AAAA,UAIO,MAAA;EAAA,SACN,IAAA;EACT,KAAA,CAAM,GAAA,EAAK,aAAA,GAAgB,
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../src/plugins/lib/types.ts"],"mappings":";;;;KAIY,aAAA;AAAA,UAEK,aAAA;EACf,YAAA;EACA,eAAA,CACE,KAAA,EAAO,KAAA,GAAQ,iBAAA,EACf,IAAA,EAAM,mBAAA;AAAA;AAAA,UAIO,MAAA;EAAA,SACN,IAAA;EACT,KAAA,CAAM,GAAA,EAAK,aAAA,GAAgB,aAAa;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export{};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logs.d.mts","names":[],"sources":["../../src/plugins/logs.ts"],"mappings":";;;;;AAyBA
|
|
1
|
+
{"version":3,"file":"logs.d.mts","names":[],"sources":["../../src/plugins/logs.ts"],"mappings":";;;;;AAyBA;;;;AA0BC;cA1BY,UAAA,EAAY,MA0BxB"}
|
package/dist/plugins/logs.mjs
CHANGED
|
@@ -1,53 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { onConsoleCall } from "../internal/console-patch.mjs";
|
|
3
|
-
import { safeStringify } from "../util/stringify.mjs";
|
|
4
|
-
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
5
|
-
//#region src/plugins/logs.ts
|
|
6
|
-
const LEVEL_TO_SEVERITY = {
|
|
7
|
-
debug: {
|
|
8
|
-
number: SeverityNumber.DEBUG,
|
|
9
|
-
text: "DEBUG"
|
|
10
|
-
},
|
|
11
|
-
info: {
|
|
12
|
-
number: SeverityNumber.INFO,
|
|
13
|
-
text: "INFO"
|
|
14
|
-
},
|
|
15
|
-
log: {
|
|
16
|
-
number: SeverityNumber.INFO,
|
|
17
|
-
text: "INFO"
|
|
18
|
-
},
|
|
19
|
-
warn: {
|
|
20
|
-
number: SeverityNumber.WARN,
|
|
21
|
-
text: "WARN"
|
|
22
|
-
},
|
|
23
|
-
error: {
|
|
24
|
-
number: SeverityNumber.ERROR,
|
|
25
|
-
text: "ERROR"
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Subscribes to the central console patch so string-only console calls
|
|
30
|
-
* land as OTel `LogRecord`s. Error-bearing calls are skipped: the
|
|
31
|
-
* `errors` plugin's subscriber on the same patch handles those as
|
|
32
|
-
* exceptions. Class boundary stays firm: errors flow as exceptions,
|
|
33
|
-
* strings flow as logs.
|
|
34
|
-
*/
|
|
35
|
-
const logsPlugin = {
|
|
36
|
-
name: "logs",
|
|
37
|
-
setup() {
|
|
38
|
-
return onConsoleCall((level, args) => {
|
|
39
|
-
if (args.some((a) => a instanceof Error)) return;
|
|
40
|
-
const kernel = activeKernel();
|
|
41
|
-
if (!kernel) return;
|
|
42
|
-
const sev = LEVEL_TO_SEVERITY[level];
|
|
43
|
-
kernel.recordLog({
|
|
44
|
-
severityText: sev.text,
|
|
45
|
-
severityNumber: sev.number,
|
|
46
|
-
body: args.length === 1 ? safeStringify(args[0]) : args.map(safeStringify).join(" "),
|
|
47
|
-
attributes: { "console.level": level }
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
//#endregion
|
|
53
|
-
export { logsPlugin as default, logsPlugin };
|
|
1
|
+
import{activeKernel}from"../internal/kernel-registry.mjs";import{onConsoleCall}from"../internal/console-patch.mjs";import{safeStringify}from"../util/stringify.mjs";import{SeverityNumber}from"@opentelemetry/api-logs";const LEVEL_TO_SEVERITY={debug:{number:SeverityNumber.DEBUG,text:`DEBUG`},info:{number:SeverityNumber.INFO,text:`INFO`},log:{number:SeverityNumber.INFO,text:`INFO`},warn:{number:SeverityNumber.WARN,text:`WARN`},error:{number:SeverityNumber.ERROR,text:`ERROR`}},logsPlugin={name:`logs`,setup(){return onConsoleCall((level,args)=>{if(args.some(a=>a instanceof Error))return;let kernel=activeKernel();if(!kernel)return;let sev=LEVEL_TO_SEVERITY[level];kernel.recordLog({severityText:sev.text,severityNumber:sev.number,body:args.length===1?safeStringify(args[0]):args.map(safeStringify).join(` `),attributes:{"console.level":level}})})}};export{logsPlugin as default,logsPlugin};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logs.mjs","names":[],"sources":["../../src/plugins/logs.ts"],"sourcesContent":["import { SeverityNumber } from \"@opentelemetry/api-logs\";\n\nimport { type ConsoleLevel, onConsoleCall } from \"../internal/console-patch.js\";\nimport { activeKernel } from \"../internal/kernel-registry.js\";\nimport { safeStringify } from \"../util/stringify.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nconst LEVEL_TO_SEVERITY: Record<\n ConsoleLevel,\n { number: SeverityNumber; text: string }\n> = {\n debug: { number: SeverityNumber.DEBUG, text: \"DEBUG\" },\n info: { number: SeverityNumber.INFO, text: \"INFO\" },\n log: { number: SeverityNumber.INFO, text: \"INFO\" },\n warn: { number: SeverityNumber.WARN, text: \"WARN\" },\n error: { number: SeverityNumber.ERROR, text: \"ERROR\" },\n};\n\n/**\n * Subscribes to the central console patch so string-only console calls\n * land as OTel `LogRecord`s. Error-bearing calls are skipped: the\n * `errors` plugin's subscriber on the same patch handles those as\n * exceptions. Class boundary stays firm: errors flow as exceptions,\n * strings flow as logs.\n */\nexport const logsPlugin: Plugin = {\n name: \"logs\",\n\n setup() {\n const unsubscribe = onConsoleCall((level, args) => {\n if (args.some((a) => a instanceof Error)) {\n return;\n }\n const kernel = activeKernel();\n if (!kernel) {\n return;\n }\n const sev = LEVEL_TO_SEVERITY[level];\n kernel.recordLog({\n severityText: sev.text,\n severityNumber: sev.number,\n body:\n args.length === 1\n ? safeStringify(args[0])\n : args.map(safeStringify).join(\" \"),\n attributes: { \"console.level\": level },\n });\n });\n\n return unsubscribe;\n },\n};\n\nexport default logsPlugin;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"logs.mjs","names":[],"sources":["../../src/plugins/logs.ts"],"sourcesContent":["import { SeverityNumber } from \"@opentelemetry/api-logs\";\n\nimport { type ConsoleLevel, onConsoleCall } from \"../internal/console-patch.js\";\nimport { activeKernel } from \"../internal/kernel-registry.js\";\nimport { safeStringify } from \"../util/stringify.js\";\nimport type { Plugin } from \"./lib/types.js\";\n\nconst LEVEL_TO_SEVERITY: Record<\n ConsoleLevel,\n { number: SeverityNumber; text: string }\n> = {\n debug: { number: SeverityNumber.DEBUG, text: \"DEBUG\" },\n info: { number: SeverityNumber.INFO, text: \"INFO\" },\n log: { number: SeverityNumber.INFO, text: \"INFO\" },\n warn: { number: SeverityNumber.WARN, text: \"WARN\" },\n error: { number: SeverityNumber.ERROR, text: \"ERROR\" },\n};\n\n/**\n * Subscribes to the central console patch so string-only console calls\n * land as OTel `LogRecord`s. Error-bearing calls are skipped: the\n * `errors` plugin's subscriber on the same patch handles those as\n * exceptions. Class boundary stays firm: errors flow as exceptions,\n * strings flow as logs.\n */\nexport const logsPlugin: Plugin = {\n name: \"logs\",\n\n setup() {\n const unsubscribe = onConsoleCall((level, args) => {\n if (args.some((a) => a instanceof Error)) {\n return;\n }\n const kernel = activeKernel();\n if (!kernel) {\n return;\n }\n const sev = LEVEL_TO_SEVERITY[level];\n kernel.recordLog({\n severityText: sev.text,\n severityNumber: sev.number,\n body:\n args.length === 1\n ? safeStringify(args[0])\n : args.map(safeStringify).join(\" \"),\n attributes: { \"console.level\": level },\n });\n });\n\n return unsubscribe;\n },\n};\n\nexport default logsPlugin;\n"],"mappings":"wNAOA,MAAM,kBAGF,CACF,MAAO,CAAE,OAAQ,eAAe,MAAO,KAAM,OAAQ,EACrD,KAAM,CAAE,OAAQ,eAAe,KAAM,KAAM,MAAO,EAClD,IAAK,CAAE,OAAQ,eAAe,KAAM,KAAM,MAAO,EACjD,KAAM,CAAE,OAAQ,eAAe,KAAM,KAAM,MAAO,EAClD,MAAO,CAAE,OAAQ,eAAe,MAAO,KAAM,OAAQ,CACvD,EASa,WAAqB,CAChC,KAAM,OAEN,OAAQ,CAqBN,OApBoB,eAAe,MAAO,OAAS,CACjD,GAAI,KAAK,KAAM,GAAM,aAAa,KAAK,EACrC,OAEF,IAAM,OAAS,aAAa,EAC5B,GAAI,CAAC,OACH,OAEF,IAAM,IAAM,kBAAkB,OAC9B,OAAO,UAAU,CACf,aAAc,IAAI,KAClB,eAAgB,IAAI,OACpB,KACE,KAAK,SAAW,EACZ,cAAc,KAAK,EAAE,EACrB,KAAK,IAAI,aAAa,EAAE,KAAK,GAAG,EACtC,WAAY,CAAE,gBAAiB,KAAM,CACvC,CAAC,CACH,CAEiB,CACnB,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rage-clicks.d.mts","names":[],"sources":["../../src/plugins/rage-clicks.ts"],"mappings":";;;cAgCa,gBAAA,EAAkB,
|
|
1
|
+
{"version":3,"file":"rage-clicks.d.mts","names":[],"sources":["../../src/plugins/rage-clicks.ts"],"mappings":";;;cAgCa,gBAAA,EAAkB,MAqE9B"}
|
|
@@ -1,55 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
//#region src/plugins/rage-clicks.ts
|
|
3
|
-
const CLICK_THRESHOLD = 3;
|
|
4
|
-
const TIME_WINDOW_MS = 800;
|
|
5
|
-
const PROXIMITY_PX = 30;
|
|
6
|
-
const TRACER_NAME = "@interfere/react/rage-clicks";
|
|
7
|
-
function distance(a, b) {
|
|
8
|
-
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
|
|
9
|
-
}
|
|
10
|
-
function selectorFor(el) {
|
|
11
|
-
if (!el) return "unknown";
|
|
12
|
-
if (el.id) return `#${el.id}`;
|
|
13
|
-
const classes = [...el.classList].slice(0, 3).join(".");
|
|
14
|
-
const tag = el.tagName.toLowerCase();
|
|
15
|
-
return classes ? `${tag}.${classes}` : tag;
|
|
16
|
-
}
|
|
17
|
-
const rageClicksPlugin = {
|
|
18
|
-
name: "rage-clicks",
|
|
19
|
-
setup() {
|
|
20
|
-
const clicks = [];
|
|
21
|
-
const onClick = (event) => {
|
|
22
|
-
const now = Date.now();
|
|
23
|
-
clicks.push({
|
|
24
|
-
x: event.clientX,
|
|
25
|
-
y: event.clientY,
|
|
26
|
-
ts: now,
|
|
27
|
-
target: event.target instanceof Element ? event.target : null
|
|
28
|
-
});
|
|
29
|
-
while (clicks.length > 0 && now - (clicks[0]?.ts ?? 0) > TIME_WINDOW_MS) clicks.shift();
|
|
30
|
-
if (clicks.length < CLICK_THRESHOLD) return;
|
|
31
|
-
const anchor = clicks[0];
|
|
32
|
-
if (!anchor) return;
|
|
33
|
-
const clustered = clicks.filter((c) => distance(anchor, c) <= PROXIMITY_PX);
|
|
34
|
-
if (clustered.length < CLICK_THRESHOLD) return;
|
|
35
|
-
const last = clustered.at(-1);
|
|
36
|
-
if (!last) return;
|
|
37
|
-
trace.getTracer(TRACER_NAME).startSpan("rage_click", { attributes: {
|
|
38
|
-
"ui.event_type": "rage_click",
|
|
39
|
-
"ui.rage_click.count": clustered.length,
|
|
40
|
-
"ui.rage_click.time_window_ms": last.ts - anchor.ts,
|
|
41
|
-
"ui.rage_click.selector": selectorFor(last.target),
|
|
42
|
-
"ui.rage_click.text": last.target?.textContent?.trim().slice(0, 120) ?? "",
|
|
43
|
-
"ui.rage_click.x": last.x,
|
|
44
|
-
"ui.rage_click.y": last.y
|
|
45
|
-
} }).end();
|
|
46
|
-
clicks.length = 0;
|
|
47
|
-
};
|
|
48
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
49
|
-
return () => {
|
|
50
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
//#endregion
|
|
55
|
-
export { rageClicksPlugin as default, rageClicksPlugin };
|
|
1
|
+
import{trace}from"@opentelemetry/api";function distance(a,b){return Math.sqrt((a.x-b.x)**2+(a.y-b.y)**2)}function selectorFor(el){if(!el)return`unknown`;if(el.id)return`#${el.id}`;let classes=[...el.classList].slice(0,3).join(`.`),tag=el.tagName.toLowerCase();return classes?`${tag}.${classes}`:tag}const rageClicksPlugin={name:`rage-clicks`,setup(){let clicks=[],onClick=event=>{let now=Date.now();for(clicks.push({x:event.clientX,y:event.clientY,ts:now,target:event.target instanceof Element?event.target:null});clicks.length>0&&now-(clicks[0]?.ts??0)>800;)clicks.shift();if(clicks.length<3)return;let anchor=clicks[0];if(!anchor)return;let clustered=clicks.filter(c=>distance(anchor,c)<=30);if(clustered.length<3)return;let last=clustered.at(-1);last&&(trace.getTracer(`@interfere/react/rage-clicks`).startSpan(`rage_click`,{attributes:{"ui.event_type":`rage_click`,"ui.rage_click.count":clustered.length,"ui.rage_click.time_window_ms":last.ts-anchor.ts,"ui.rage_click.selector":selectorFor(last.target),"ui.rage_click.text":last.target?.textContent?.trim().slice(0,120)??``,"ui.rage_click.x":last.x,"ui.rage_click.y":last.y}}).end(),clicks.length=0)};return document.addEventListener(`click`,onClick,{capture:!0}),()=>{document.removeEventListener(`click`,onClick,{capture:!0})}}};export{rageClicksPlugin as default,rageClicksPlugin};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rage-clicks.mjs","names":[],"sources":["../../src/plugins/rage-clicks.ts"],"sourcesContent":["import { trace } from \"@opentelemetry/api\";\n\nimport type { Plugin } from \"./lib/types.js\";\n\nconst CLICK_THRESHOLD = 3;\nconst TIME_WINDOW_MS = 800;\nconst PROXIMITY_PX = 30;\nconst TRACER_NAME = \"@interfere/react/rage-clicks\";\n\ninterface Click {\n target: Element | null;\n ts: number;\n x: number;\n y: number;\n}\n\nfunction distance(a: Click, b: Click): number {\n return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n}\n\nfunction selectorFor(el: Element | null): string {\n if (!el) {\n return \"unknown\";\n }\n if (el.id) {\n return `#${el.id}`;\n }\n const classes = [...el.classList].slice(0, 3).join(\".\");\n const tag = el.tagName.toLowerCase();\n return classes ? `${tag}.${classes}` : tag;\n}\n\nexport const rageClicksPlugin: Plugin = {\n name: \"rage-clicks\",\n\n setup() {\n const clicks: Click[] = [];\n\n const onClick = (event: MouseEvent) => {\n const now = Date.now();\n clicks.push({\n x: event.clientX,\n y: event.clientY,\n ts: now,\n target: event.target instanceof Element ? event.target : null,\n });\n\n // Prune stale clicks\n while (clicks.length > 0 && now - (clicks[0]?.ts ?? 0) > TIME_WINDOW_MS) {\n clicks.shift();\n }\n\n if (clicks.length < CLICK_THRESHOLD) {\n return;\n }\n\n // Check proximity — all clicks within PROXIMITY_PX of the first\n const anchor = clicks[0];\n if (!anchor) {\n return;\n }\n const clustered = clicks.filter(\n (c) => distance(anchor, c) <= PROXIMITY_PX\n );\n if (clustered.length < CLICK_THRESHOLD) {\n return;\n }\n\n const last = clustered.at(-1);\n if (!last) {\n return;\n }\n\n // Emit as a discrete OTel span. The cluster of clicks is a single\n // user-experience event; a span (rather than a span event on\n // whatever's currently active) keeps the rage-click correlatable\n // by `ui.event_type=rage_click` regardless of whether a navigation\n // / interaction span happens to be open at click time.\n const span = trace.getTracer(TRACER_NAME).startSpan(\"rage_click\", {\n attributes: {\n \"ui.event_type\": \"rage_click\",\n \"ui.rage_click.count\": clustered.length,\n \"ui.rage_click.time_window_ms\": last.ts - anchor.ts,\n \"ui.rage_click.selector\": selectorFor(last.target),\n \"ui.rage_click.text\":\n last.target?.textContent?.trim().slice(0, 120) ?? \"\",\n \"ui.rage_click.x\": last.x,\n \"ui.rage_click.y\": last.y,\n },\n });\n span.end();\n\n clicks.length = 0;\n };\n\n document.addEventListener(\"click\", onClick, { capture: true });\n\n return () => {\n document.removeEventListener(\"click\", onClick, { capture: true });\n };\n },\n};\n\nexport default rageClicksPlugin;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"rage-clicks.mjs","names":[],"sources":["../../src/plugins/rage-clicks.ts"],"sourcesContent":["import { trace } from \"@opentelemetry/api\";\n\nimport type { Plugin } from \"./lib/types.js\";\n\nconst CLICK_THRESHOLD = 3;\nconst TIME_WINDOW_MS = 800;\nconst PROXIMITY_PX = 30;\nconst TRACER_NAME = \"@interfere/react/rage-clicks\";\n\ninterface Click {\n target: Element | null;\n ts: number;\n x: number;\n y: number;\n}\n\nfunction distance(a: Click, b: Click): number {\n return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);\n}\n\nfunction selectorFor(el: Element | null): string {\n if (!el) {\n return \"unknown\";\n }\n if (el.id) {\n return `#${el.id}`;\n }\n const classes = [...el.classList].slice(0, 3).join(\".\");\n const tag = el.tagName.toLowerCase();\n return classes ? `${tag}.${classes}` : tag;\n}\n\nexport const rageClicksPlugin: Plugin = {\n name: \"rage-clicks\",\n\n setup() {\n const clicks: Click[] = [];\n\n const onClick = (event: MouseEvent) => {\n const now = Date.now();\n clicks.push({\n x: event.clientX,\n y: event.clientY,\n ts: now,\n target: event.target instanceof Element ? event.target : null,\n });\n\n // Prune stale clicks\n while (clicks.length > 0 && now - (clicks[0]?.ts ?? 0) > TIME_WINDOW_MS) {\n clicks.shift();\n }\n\n if (clicks.length < CLICK_THRESHOLD) {\n return;\n }\n\n // Check proximity — all clicks within PROXIMITY_PX of the first\n const anchor = clicks[0];\n if (!anchor) {\n return;\n }\n const clustered = clicks.filter(\n (c) => distance(anchor, c) <= PROXIMITY_PX\n );\n if (clustered.length < CLICK_THRESHOLD) {\n return;\n }\n\n const last = clustered.at(-1);\n if (!last) {\n return;\n }\n\n // Emit as a discrete OTel span. The cluster of clicks is a single\n // user-experience event; a span (rather than a span event on\n // whatever's currently active) keeps the rage-click correlatable\n // by `ui.event_type=rage_click` regardless of whether a navigation\n // / interaction span happens to be open at click time.\n const span = trace.getTracer(TRACER_NAME).startSpan(\"rage_click\", {\n attributes: {\n \"ui.event_type\": \"rage_click\",\n \"ui.rage_click.count\": clustered.length,\n \"ui.rage_click.time_window_ms\": last.ts - anchor.ts,\n \"ui.rage_click.selector\": selectorFor(last.target),\n \"ui.rage_click.text\":\n last.target?.textContent?.trim().slice(0, 120) ?? \"\",\n \"ui.rage_click.x\": last.x,\n \"ui.rage_click.y\": last.y,\n },\n });\n span.end();\n\n clicks.length = 0;\n };\n\n document.addEventListener(\"click\", onClick, { capture: true });\n\n return () => {\n document.removeEventListener(\"click\", onClick, { capture: true });\n };\n },\n};\n\nexport default rageClicksPlugin;\n"],"mappings":"sCAgBA,SAAS,SAAS,EAAU,EAAkB,CAC5C,OAAO,KAAK,MAAM,EAAE,EAAI,EAAE,IAAM,GAAK,EAAE,EAAI,EAAE,IAAM,CAAC,CACtD,CAEA,SAAS,YAAY,GAA4B,CAC/C,GAAI,CAAC,GACH,MAAO,UAET,GAAI,GAAG,GACL,MAAO,IAAI,GAAG,KAEhB,IAAM,QAAU,CAAC,GAAG,GAAG,SAAS,EAAE,MAAM,EAAG,CAAC,EAAE,KAAK,GAAG,EAChD,IAAM,GAAG,QAAQ,YAAY,EACnC,OAAO,QAAU,GAAG,IAAI,GAAG,UAAY,GACzC,CAEA,MAAa,iBAA2B,CACtC,KAAM,cAEN,OAAQ,CACN,IAAM,OAAkB,CAAC,EAEnB,QAAW,OAAsB,CACrC,IAAM,IAAM,KAAK,IAAI,EASrB,IARA,OAAO,KAAK,CACV,EAAG,MAAM,QACT,EAAG,MAAM,QACT,GAAI,IACJ,OAAQ,MAAM,kBAAkB,QAAU,MAAM,OAAS,IAC3D,CAAC,EAGM,OAAO,OAAS,GAAK,KAAO,OAAO,IAAI,IAAM,GAAK,KACvD,OAAO,MAAM,EAGf,GAAI,OAAO,OAAS,EAClB,OAIF,IAAM,OAAS,OAAO,GACtB,GAAI,CAAC,OACH,OAEF,IAAM,UAAY,OAAO,OACtB,GAAM,SAAS,OAAQ,CAAC,GAAK,EAChC,EACA,GAAI,UAAU,OAAS,EACrB,OAGF,IAAM,KAAO,UAAU,GAAG,EAAE,EACvB,OAqBL,MAZmB,UAAU,8BAAW,EAAE,UAAU,aAAc,CAChE,WAAY,CACV,gBAAiB,aACjB,sBAAuB,UAAU,OACjC,+BAAgC,KAAK,GAAK,OAAO,GACjD,yBAA0B,YAAY,KAAK,MAAM,EACjD,qBACE,KAAK,QAAQ,aAAa,KAAK,EAAE,MAAM,EAAG,GAAG,GAAK,GACpD,kBAAmB,KAAK,EACxB,kBAAmB,KAAK,CAC1B,CACF,CACG,EAAE,IAAI,EAET,OAAO,OAAS,EAClB,EAIA,OAFA,SAAS,iBAAiB,QAAS,QAAS,CAAE,QAAS,EAAK,CAAC,MAEhD,CACX,SAAS,oBAAoB,QAAS,QAAS,CAAE,QAAS,EAAK,CAAC,CAClE,CACF,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.d.mts","names":[],"sources":["../../src/plugins/replay.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"replay.d.mts","names":[],"sources":["../../src/plugins/replay.ts"],"mappings":";;;cAyFa,YAAA,EAAc,MA4E1B"}
|
package/dist/plugins/replay.mjs
CHANGED
|
@@ -1,101 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { resolveTargets } from "../internal/config.mjs";
|
|
3
|
-
import { onPageHidden } from "../internal/page-lifecycle.mjs";
|
|
4
|
-
import { trace } from "@opentelemetry/api";
|
|
5
|
-
//#region src/plugins/replay.ts
|
|
6
|
-
const log = createLogger("replay");
|
|
7
|
-
const FLUSH_INTERVAL_MS = 1e4;
|
|
8
|
-
const UPLOAD_PATH_PREFIX = "/v2/replay/upload/";
|
|
9
|
-
const TRACER_NAME = "@interfere/react/replay";
|
|
10
|
-
const TRAILING_SLASH_RE = /\/$/;
|
|
11
|
-
/**
|
|
12
|
-
* POSTs the chunk's events array to the collector. The collector writes
|
|
13
|
-
* to R2 server-side and returns the object key we stamp onto the
|
|
14
|
-
* `replay.chunk` span event. Single round trip; if the request fails,
|
|
15
|
-
* the chunk is dropped — replay is best-effort and the customer's next
|
|
16
|
-
* session will still record cleanly.
|
|
17
|
-
*/
|
|
18
|
-
async function deliverChunk({ ctx, uploadBaseUrl, authHeaders, payload }) {
|
|
19
|
-
const sessionId = ctx.getSessionId();
|
|
20
|
-
if (!sessionId) return;
|
|
21
|
-
const body = JSON.stringify(payload.events);
|
|
22
|
-
const sizeBytes = body.length;
|
|
23
|
-
const res = await fetch(`${uploadBaseUrl}${encodeURIComponent(sessionId)}`, {
|
|
24
|
-
method: "POST",
|
|
25
|
-
headers: {
|
|
26
|
-
...authHeaders,
|
|
27
|
-
"content-type": "application/json"
|
|
28
|
-
},
|
|
29
|
-
body
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
log.warn("replay chunk upload rejected (%d)", res.status);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const { objectKey } = await res.json();
|
|
36
|
-
trace.getTracer(TRACER_NAME).startSpan("replay.chunk", { attributes: {
|
|
37
|
-
"replay.chunk.uri": objectKey,
|
|
38
|
-
"replay.chunk.size_bytes": sizeBytes,
|
|
39
|
-
"replay.chunk.event_count": payload.events.length,
|
|
40
|
-
...payload.firstTs === null ? {} : { "replay.chunk.first_event_ts": payload.firstTs },
|
|
41
|
-
...payload.lastTs === null ? {} : { "replay.chunk.last_event_ts": payload.lastTs }
|
|
42
|
-
} }).end();
|
|
43
|
-
}
|
|
44
|
-
const replayPlugin = {
|
|
45
|
-
name: "replay",
|
|
46
|
-
setup(ctx) {
|
|
47
|
-
const targets = resolveTargets();
|
|
48
|
-
const uploadBaseUrl = `${targets.collectorBaseUrl.replace(TRAILING_SLASH_RE, "")}${UPLOAD_PATH_PREFIX}`;
|
|
49
|
-
const authHeaders = Object.fromEntries(targets.ingest.headers.entries());
|
|
50
|
-
let stopFn = null;
|
|
51
|
-
let events = [];
|
|
52
|
-
let flushTimer = null;
|
|
53
|
-
let firstTs = null;
|
|
54
|
-
let lastTs = null;
|
|
55
|
-
const flush = () => {
|
|
56
|
-
if (events.length === 0) return;
|
|
57
|
-
const chunk = events;
|
|
58
|
-
events = [];
|
|
59
|
-
const payload = {
|
|
60
|
-
events: chunk,
|
|
61
|
-
firstTs,
|
|
62
|
-
lastTs
|
|
63
|
-
};
|
|
64
|
-
firstTs = null;
|
|
65
|
-
lastTs = null;
|
|
66
|
-
deliverChunk({
|
|
67
|
-
ctx,
|
|
68
|
-
uploadBaseUrl,
|
|
69
|
-
authHeaders,
|
|
70
|
-
payload
|
|
71
|
-
}).catch((error) => {
|
|
72
|
-
log.warn("replay chunk dropped: %o", error);
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
let unsubscribeHidden = null;
|
|
76
|
-
const init = async () => {
|
|
77
|
-
try {
|
|
78
|
-
stopFn = (await import("rrweb")).record({ emit(event) {
|
|
79
|
-
const ts = Date.now();
|
|
80
|
-
if (firstTs === null) firstTs = ts;
|
|
81
|
-
lastTs = ts;
|
|
82
|
-
events.push(JSON.stringify(event));
|
|
83
|
-
} }) ?? null;
|
|
84
|
-
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
85
|
-
unsubscribeHidden = onPageHidden(flush);
|
|
86
|
-
log.debug("recording started");
|
|
87
|
-
} catch {
|
|
88
|
-
log.error("rrweb failed to load, replay disabled");
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
init();
|
|
92
|
-
return () => {
|
|
93
|
-
flush();
|
|
94
|
-
stopFn?.();
|
|
95
|
-
if (flushTimer) clearInterval(flushTimer);
|
|
96
|
-
unsubscribeHidden?.();
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
//#endregion
|
|
101
|
-
export { replayPlugin as default, replayPlugin };
|
|
1
|
+
import{createLogger}from"../util/log.mjs";import{appendPathBeforeQuery,resolveTargets}from"../internal/config.mjs";import{onPageHidden}from"../internal/page-lifecycle.mjs";import{trace}from"@opentelemetry/api";const log=createLogger(`replay`);async function deliverChunk({ctx,uploadBaseUrl,authHeaders,payload}){let sessionId=ctx.getSessionId();if(!sessionId)return;let body=JSON.stringify(payload.events),sizeBytes=body.length,res=await fetch(appendPathBeforeQuery(uploadBaseUrl,`/v2/replay/upload/${encodeURIComponent(sessionId)}`),{method:`POST`,headers:{...authHeaders,"content-type":`application/json`},body});if(!res.ok){log.warn(`replay chunk upload rejected (%d)`,res.status);return}let{objectKey}=await res.json();trace.getTracer(`@interfere/react/replay`).startSpan(`replay.chunk`,{attributes:{"replay.chunk.uri":objectKey,"replay.chunk.size_bytes":sizeBytes,"replay.chunk.event_count":payload.events.length,...payload.firstTs===null?{}:{"replay.chunk.first_event_ts":payload.firstTs},...payload.lastTs===null?{}:{"replay.chunk.last_event_ts":payload.lastTs}}}).end()}const replayPlugin={name:`replay`,setup(ctx){let targets=resolveTargets(),uploadBaseUrl=targets.collectorBaseUrl,authHeaders=Object.fromEntries(targets.ingest.headers.entries()),stopFn=null,events=[],flushTimer=null,firstTs=null,lastTs=null,flush=()=>{if(events.length===0)return;let chunk=events;events=[];let payload={events:chunk,firstTs,lastTs};firstTs=null,lastTs=null,deliverChunk({ctx,uploadBaseUrl,authHeaders,payload}).catch(error=>{log.warn(`replay chunk dropped: %o`,error)})},unsubscribeHidden=null;return(async()=>{try{stopFn=(await import(`rrweb`)).record({emit(event){let ts=Date.now();firstTs===null&&(firstTs=ts),lastTs=ts,events.push(JSON.stringify(event))}})??null,flushTimer=setInterval(flush,1e4),unsubscribeHidden=onPageHidden(flush),log.debug(`recording started`)}catch{log.error(`rrweb failed to load, replay disabled`)}})(),()=>{flush(),stopFn?.(),flushTimer&&clearInterval(flushTimer),unsubscribeHidden?.()}}};export{replayPlugin as default,replayPlugin};
|