@interfere/react 9.0.2 → 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/README.md +4 -4
- package/dist/api.d.mts +25 -0
- package/dist/api.d.mts.map +1 -0
- package/dist/api.mjs +1 -0
- package/dist/api.mjs.map +1 -0
- package/dist/error-boundary.d.mts +10 -4
- package/dist/error-boundary.d.mts.map +1 -1
- package/dist/error-boundary.mjs +1 -39
- package/dist/error-boundary.mjs.map +1 -1
- package/dist/internal/browser-context.d.mts +6 -0
- package/dist/internal/browser-context.d.mts.map +1 -0
- package/dist/internal/browser-context.mjs +1 -0
- package/dist/internal/browser-context.mjs.map +1 -0
- package/dist/internal/capture-boundary.d.mts +4 -1
- package/dist/internal/capture-boundary.d.mts.map +1 -1
- package/dist/internal/capture-boundary.mjs +1 -44
- package/dist/internal/capture-boundary.mjs.map +1 -1
- package/dist/internal/capture.d.mts +16 -5
- package/dist/internal/capture.d.mts.map +1 -1
- package/dist/internal/capture.mjs +1 -23
- package/dist/internal/capture.mjs.map +1 -1
- package/dist/internal/config.d.mts +22 -4
- 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 -25
- package/dist/internal/consent.mjs.map +1 -1
- package/dist/internal/console-patch.d.mts +19 -0
- package/dist/internal/console-patch.d.mts.map +1 -0
- package/dist/internal/console-patch.mjs +1 -0
- package/dist/internal/console-patch.mjs.map +1 -0
- package/dist/internal/dom/actionable.d.mts +27 -0
- package/dist/internal/dom/actionable.d.mts.map +1 -0
- package/dist/internal/dom/actionable.mjs +1 -0
- package/dist/internal/dom/actionable.mjs.map +1 -0
- package/dist/internal/kernel-registry.d.mts +8 -0
- package/dist/internal/kernel-registry.d.mts.map +1 -0
- package/dist/internal/kernel-registry.mjs +1 -0
- package/dist/internal/kernel-registry.mjs.map +1 -0
- package/dist/internal/kernel.d.mts +267 -0
- package/dist/internal/kernel.d.mts.map +1 -0
- package/dist/internal/kernel.mjs +1 -0
- package/dist/internal/kernel.mjs.map +1 -0
- package/dist/internal/otel/exporter.d.mts +85 -0
- package/dist/internal/otel/exporter.d.mts.map +1 -0
- package/dist/internal/otel/exporter.mjs +1 -0
- package/dist/internal/otel/exporter.mjs.map +1 -0
- package/dist/internal/otel/index.d.mts +6 -0
- package/dist/internal/otel/index.mjs +1 -0
- package/dist/internal/otel/instrumentations.d.mts +42 -0
- package/dist/internal/otel/instrumentations.d.mts.map +1 -0
- package/dist/internal/otel/instrumentations.mjs +1 -0
- package/dist/internal/otel/instrumentations.mjs.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts +32 -0
- package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -0
- package/dist/internal/otel/page-scope-context-manager.mjs +1 -0
- package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -0
- package/dist/internal/otel/propagation.d.mts +21 -0
- package/dist/internal/otel/propagation.d.mts.map +1 -0
- package/dist/internal/otel/propagation.mjs +1 -0
- package/dist/internal/otel/propagation.mjs.map +1 -0
- package/dist/internal/otel/provider.d.mts +106 -0
- package/dist/internal/otel/provider.d.mts.map +1 -0
- package/dist/internal/otel/provider.mjs +1 -0
- package/dist/internal/otel/provider.mjs.map +1 -0
- package/dist/internal/otel/web-vitals.d.mts +35 -0
- package/dist/internal/otel/web-vitals.d.mts.map +1 -0
- package/dist/internal/otel/web-vitals.mjs +1 -0
- package/dist/internal/otel/web-vitals.mjs.map +1 -0
- package/dist/internal/page-lifecycle.d.mts +21 -0
- package/dist/internal/page-lifecycle.d.mts.map +1 -0
- package/dist/internal/page-lifecycle.mjs +1 -0
- package/dist/internal/page-lifecycle.mjs.map +1 -0
- package/dist/internal/plugin-runtime.d.mts +0 -2
- package/dist/internal/plugin-runtime.d.mts.map +1 -1
- package/dist/internal/plugin-runtime.mjs +1 -107
- package/dist/internal/plugin-runtime.mjs.map +1 -1
- package/dist/internal/react-context.d.mts +44 -0
- package/dist/internal/react-context.d.mts.map +1 -0
- package/dist/internal/react-context.mjs +1 -0
- package/dist/internal/react-context.mjs.map +1 -0
- package/dist/internal/sw.d.mts +22 -2
- package/dist/internal/sw.d.mts.map +1 -1
- package/dist/internal/sw.mjs +1 -10
- package/dist/internal/sw.mjs.map +1 -1
- package/dist/internal/version.d.mts +3 -1
- package/dist/internal/version.d.mts.map +1 -1
- package/dist/internal/version.mjs +1 -5
- package/dist/internal/version.mjs.map +1 -1
- package/dist/internal/wrapper-singleton.d.mts +47 -0
- package/dist/internal/wrapper-singleton.d.mts.map +1 -0
- package/dist/internal/wrapper-singleton.mjs +1 -0
- package/dist/internal/wrapper-singleton.mjs.map +1 -0
- package/dist/package.mjs +1 -5
- package/dist/plugins/errors.d.mts.map +1 -1
- package/dist/plugins/errors.mjs +1 -91
- package/dist/plugins/errors.mjs.map +1 -1
- package/dist/plugins/lib/loader.d.mts +1 -2
- package/dist/plugins/lib/loader.d.mts.map +1 -1
- package/dist/plugins/lib/loader.mjs +1 -43
- package/dist/plugins/lib/loader.mjs.map +1 -1
- package/dist/plugins/lib/types.d.mts +3 -2
- package/dist/plugins/lib/types.d.mts.map +1 -1
- package/dist/plugins/lib/types.mjs +1 -1
- package/dist/plugins/logs.d.mts +13 -0
- package/dist/plugins/logs.d.mts.map +1 -0
- package/dist/plugins/logs.mjs +1 -0
- package/dist/plugins/logs.mjs.map +1 -0
- package/dist/plugins/rage-clicks.d.mts.map +1 -1
- package/dist/plugins/rage-clicks.mjs +1 -53
- package/dist/plugins/rage-clicks.mjs.map +1 -1
- package/dist/plugins/replay.d.mts.map +1 -1
- package/dist/plugins/replay.mjs +1 -62
- package/dist/plugins/replay.mjs.map +1 -1
- package/dist/provider.d.mts +11 -20
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +1 -32
- package/dist/provider.mjs.map +1 -1
- package/dist/react-error-handler.d.mts +21 -5
- package/dist/react-error-handler.d.mts.map +1 -1
- package/dist/react-error-handler.mjs +1 -54
- package/dist/react-error-handler.mjs.map +1 -1
- package/dist/sw.d.mts +2 -0
- package/dist/sw.mjs +2 -0
- package/dist/tracking/api.d.mts +41 -15
- package/dist/tracking/api.d.mts.map +1 -1
- package/dist/tracking/api.mjs +1 -134
- package/dist/tracking/api.mjs.map +1 -1
- package/dist/tracking/device.d.mts +30 -7
- package/dist/tracking/device.d.mts.map +1 -1
- package/dist/tracking/device.mjs +1 -80
- package/dist/tracking/device.mjs.map +1 -1
- package/dist/tracking/geo.d.mts +11 -3
- package/dist/tracking/geo.d.mts.map +1 -1
- package/dist/tracking/geo.mjs +2 -44
- package/dist/tracking/geo.mjs.map +1 -1
- package/dist/tracking/session.d.mts +3 -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 +10 -0
- package/dist/util/bot.d.mts.map +1 -0
- package/dist/util/bot.mjs +1 -0
- package/dist/util/bot.mjs.map +1 -0
- package/dist/util/global.d.mts +10 -0
- package/dist/util/global.d.mts.map +1 -0
- package/dist/util/global.mjs +1 -0
- package/dist/util/global.mjs.map +1 -0
- package/dist/util/log.d.mts.map +1 -1
- package/dist/util/log.mjs +1 -37
- package/dist/util/log.mjs.map +1 -1
- package/dist/util/stringify.d.mts +9 -0
- package/dist/util/stringify.d.mts.map +1 -0
- package/dist/util/stringify.mjs +1 -0
- package/dist/util/stringify.mjs.map +1 -0
- package/package.json +79 -25
- package/dist/internal/client.d.mts +0 -48
- package/dist/internal/client.d.mts.map +0 -1
- package/dist/internal/client.mjs +0 -146
- package/dist/internal/client.mjs.map +0 -1
- package/dist/internal/context.d.mts +0 -6
- package/dist/internal/context.d.mts.map +0 -1
- package/dist/internal/context.mjs +0 -32
- package/dist/internal/context.mjs.map +0 -1
- package/dist/internal/envelope.d.mts +0 -15
- package/dist/internal/envelope.d.mts.map +0 -1
- package/dist/internal/envelope.mjs +0 -24
- package/dist/internal/envelope.mjs.map +0 -1
- package/dist/internal/errors.d.mts +0 -4
- package/dist/internal/errors.d.mts.map +0 -1
- package/dist/internal/errors.mjs +0 -4
- package/dist/internal/errors.mjs.map +0 -1
- package/dist/plugins/device.d.mts +0 -6
- package/dist/plugins/device.d.mts.map +0 -1
- package/dist/plugins/device.mjs +0 -13
- package/dist/plugins/device.mjs.map +0 -1
- package/dist/plugins/pages.d.mts +0 -6
- package/dist/plugins/pages.d.mts.map +0 -1
- package/dist/plugins/pages.mjs +0 -102
- package/dist/plugins/pages.mjs.map +0 -1
- package/dist/transport/http.d.mts +0 -25
- package/dist/transport/http.d.mts.map +0 -1
- package/dist/transport/http.mjs +0 -80
- package/dist/transport/http.mjs.map +0 -1
- package/dist/transport/queue.d.mts +0 -34
- package/dist/transport/queue.d.mts.map +0 -1
- package/dist/transport/queue.mjs +0 -100
- package/dist/transport/queue.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentations.mjs","names":[],"sources":["../../../src/internal/otel/instrumentations.ts"],"sourcesContent":["import type { Span } from \"@opentelemetry/api\";\nimport { registerInstrumentations } from \"@opentelemetry/instrumentation\";\nimport {\n BrowserNavigationInstrumentation,\n defaultSanitizeUrl,\n} from \"@opentelemetry/instrumentation-browser-navigation\";\nimport { DocumentLoadInstrumentation } from \"@opentelemetry/instrumentation-document-load\";\nimport { FetchInstrumentation } from \"@opentelemetry/instrumentation-fetch\";\nimport {\n LongTaskInstrumentation,\n type ObserverCallbackInformation,\n} from \"@opentelemetry/instrumentation-long-task\";\nimport { UserInteractionInstrumentation } from \"@opentelemetry/instrumentation-user-interaction\";\nimport { XMLHttpRequestInstrumentation } from \"@opentelemetry/instrumentation-xml-http-request\";\nimport type { WebTracerProvider } from \"@opentelemetry/sdk-trace-web\";\n\nimport { describeActionable, isActionable } from \"../dom/actionable.js\";\n\nconst ATTR_HTTP_ROUTE = \"http.route\" as const;\nconst ATTR_URL_FULL = \"url.full\" as const;\nconst ATTR_URL_PATH = \"url.path\" as const;\n\n/**\n * Mirrors the `semconvStabilityOptIn` constant from the internal\n * `observability/browser/rum.ts`. Pins the HTTP attribute set to the\n * stable-namespace conventions (`url.path`, `server.address`, …) so\n * dashboards that filter on those keys aren't broken by minor\n * upgrades of the instrumentation packages.\n */\nconst SEMCONV_STABILITY_OPT_IN =\n \"http,database,messaging,gen_ai_latest_experimental\";\n\n/**\n * Hosts/paths whose CLIENT fetch spans we never want to emit: they\n * always become 1-span orphan traces (no traceparent propagation to\n * 3rd-party origins) and clutter live tail. Conservative, customer-\n * generic list — common analytics / ads / auth / replay vendors that\n * many apps use. Customer-specific noise (feature-flag CDNs, our own\n * BetterStack, etc.) belongs in the customer's own `ignoreUrls` config,\n * not hardcoded into a public SDK.\n */\nconst THIRD_PARTY_IGNORE: RegExp[] = [\n /(?:^|\\/\\/)clerk\\./,\n /\\.clerk\\./,\n /\\.sentry\\./,\n /\\.intercom\\./,\n /\\.posthog\\./,\n /\\.googletagmanager\\.com/,\n /\\.google-analytics\\.com/,\n /\\.googleapis\\.com/,\n /\\.doubleclick\\.net/,\n /\\.facebook\\.com/,\n /\\.fbcdn\\.net/,\n /\\.analytics\\.google\\.com/,\n /px\\.ads\\.linkedin\\.com/,\n /[?&]_rsc=/,\n];\n\nconst NEVER_MATCH = /^$/;\nconst ESCAPE_RE = /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction sameOriginPattern(): RegExp {\n if (typeof window === \"undefined\") {\n return NEVER_MATCH;\n }\n const { protocol, host } = window.location;\n return new RegExp(`^${protocol}//${host.replace(ESCAPE_RE, \"\\\\$&\")}`);\n}\n\nfunction stampInteractionAttrs(\n eventType: string,\n element: HTMLElement,\n span: Span\n): void {\n const desc = describeActionable(element);\n span.setAttribute(\"ui.event_type\", eventType);\n span.setAttribute(\"ui.target.tag\", desc.tag);\n if (desc.id) {\n span.setAttribute(\"ui.target.id\", desc.id);\n }\n if (desc.name) {\n span.setAttribute(\"ui.target.name\", desc.name);\n }\n if (desc.role) {\n span.setAttribute(\"ui.target.role\", desc.role);\n }\n if (desc.ariaLabel) {\n span.setAttribute(\"ui.target.aria_label\", desc.ariaLabel);\n }\n // Truncated visible label — useful for triage without exfiltrating\n // full DOM contents. Only meaningful on the direct target.\n if (desc.text) {\n span.setAttribute(\"ui.target.text\", desc.text);\n }\n}\n\nfunction stampUrlAttrs(\n span: Span,\n resolveRoute: ((pathname: string) => string | undefined) | undefined\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n const pathname = window.location.pathname;\n const route = resolveRoute?.(pathname);\n span.setAttribute(ATTR_URL_PATH, route ?? pathname);\n span.setAttribute(ATTR_URL_FULL, window.location.href);\n if (route) {\n span.setAttribute(ATTR_HTTP_ROUTE, route);\n }\n}\n\nexport interface InstrumentationsInput {\n /**\n * URLs the SDK exempts from `fetch` and `XHR` instrumentation —\n * typically our own ingest/OTLP endpoints, so the SDK doesn't trace\n * its own export requests in an infinite loop. Combined with the\n * `THIRD_PARTY_IGNORE` defaults and the customer's `ignoreUrls`.\n */\n ignoreUrls: (string | RegExp)[];\n /**\n * Customer-supplied URL patterns the fetch + XHR instrumentations\n * inject `traceparent` + `baggage` headers on. Same-origin requests\n * always propagate.\n */\n propagateContextUrls?: (string | RegExp)[];\n /**\n * Pathname → low-cardinality route template (e.g. `/blog/[slug]`).\n * Drives span renaming + `url.path` / `http.route` attrs across\n * fetch, document-load, user-interaction, and long-task.\n */\n resolveRoute?: (pathname: string) => string | undefined;\n /**\n * The kernel's private provider — instrumentations register against\n * it, not the global one, so customer OTel setups stay untouched.\n */\n tracerProvider: WebTracerProvider;\n}\n\n/**\n * Registers every browser auto-instrumentation against the kernel's\n * private provider. Every instrumentation is enriched with the same\n * route-template-aware URL attributes so dashboards can slice fetch /\n * interaction / long-task / resource-load by `http.route` without\n * cardinality blow-up from raw pathnames.\n *\n * Returns a disposer that unregisters everything; called by\n * `kernel.dispose()`.\n */\nexport function registerBundledInstrumentations(\n input: InstrumentationsInput\n): () => void {\n const { tracerProvider, ignoreUrls, propagateContextUrls = [] } = input;\n const resolveRoute = input.resolveRoute;\n const origin = sameOriginPattern();\n const fetchIgnore = [...ignoreUrls, ...THIRD_PARTY_IGNORE];\n\n return registerInstrumentations({\n tracerProvider,\n instrumentations: [\n new FetchInstrumentation({\n propagateTraceHeaderCorsUrls: [origin, ...propagateContextUrls],\n ignoreUrls: fetchIgnore,\n semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,\n measureRequestSize: true,\n applyCustomAttributesOnSpan: (\n span: Span,\n request: Request | RequestInit\n ) => {\n const url =\n request instanceof Request ? request.url : request?.toString();\n if (typeof url !== \"string\") {\n return;\n }\n try {\n const parsed = new URL(url);\n span.setAttribute(ATTR_URL_PATH, parsed.pathname);\n const isTracked =\n origin.test(url) ||\n propagateContextUrls.some((re) =>\n re instanceof RegExp ? re.test(url) : url.includes(re)\n );\n if (isTracked) {\n const method =\n (request instanceof Request ? request.method : undefined) ??\n \"GET\";\n const route = resolveRoute?.(parsed.pathname);\n if (route) {\n span.setAttribute(ATTR_HTTP_ROUTE, route);\n }\n span.updateName(`${method} ${route ?? parsed.pathname}`);\n }\n } catch {\n return;\n }\n },\n }),\n new XMLHttpRequestInstrumentation({\n propagateTraceHeaderCorsUrls: [origin, ...propagateContextUrls],\n ignoreUrls: fetchIgnore,\n semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,\n }),\n new DocumentLoadInstrumentation({\n semconvStabilityOptIn: SEMCONV_STABILITY_OPT_IN,\n // Default `resourceFetch` span name is the literal string\n // `\"resourceFetch\"` for every entry — useless when a single\n // page load emits 30 of them. Replace with the pathname (or\n // origin+path for cross-origin) so the waterfall is scannable.\n applyCustomAttributesOnSpan: {\n resourceFetch: (span, entry) => {\n const name = entry?.name;\n if (typeof name !== \"string\") {\n return;\n }\n try {\n const url = new URL(name);\n const isCrossOrigin =\n typeof window !== \"undefined\" &&\n url.origin !== window.location.origin;\n span.updateName(\n isCrossOrigin\n ? `resource: ${url.host}${url.pathname}`\n : `resource: ${url.pathname}`\n );\n span.setAttribute(ATTR_URL_FULL, name);\n span.setAttribute(ATTR_URL_PATH, url.pathname);\n span.setAttribute(\"server.address\", url.host);\n } catch {\n // Malformed/relative URL — leave the default name and\n // let the entry's own attributes carry whatever info\n // the SDK already stamped.\n }\n },\n },\n }),\n // `BrowserNavigationInstrumentation` emits a `browser.navigation`\n // log record per hard navigation (page load) and soft navigation\n // (history.pushState, hash change, back/forward). The default\n // sanitizer redacts credentials in the URL and common sensitive\n // query params (`token`, `api_key`, `secret`, …) before emit.\n new BrowserNavigationInstrumentation({\n sanitizeUrl: defaultSanitizeUrl,\n }),\n new UserInteractionInstrumentation({\n eventNames: [\"click\", \"submit\"],\n // Despite the name, `shouldPreventSpanCreation` is also the\n // hook for *enriching* spans (return falsy → create the span;\n // the callback may set attributes on the way through). We use\n // it as both: stamp a stable target descriptor for\n // dashboarding, then suppress spans on elements that aren't\n // actionable (scroll containers, body clicks, etc.).\n shouldPreventSpanCreation: (eventType, element, span) => {\n if (eventType === \"click\" && !isActionable(element)) {\n return true;\n }\n stampInteractionAttrs(eventType, element, span);\n stampUrlAttrs(span, resolveRoute);\n return false;\n },\n }),\n // Long-task spans default to a generic `longtask` name with\n // attribution embedded as attributes — but no page context.\n // Empty `url.path` makes it impossible to triage which routes\n // are jank-heavy. Stamp it in the observer callback so\n // dashboards can slice by route.\n new LongTaskInstrumentation({\n observerCallback: (span, info: ObserverCallbackInformation) => {\n stampUrlAttrs(span, resolveRoute);\n // PerformanceLongTaskTiming attribution[0].containerType is\n // the surface that hosted the offending task. Most are\n // `\"window\"` (main frame); the few that aren't (`\"iframe\"`,\n // `\"embed\"`, `\"object\"`) are the actionable ones — surface\n // them as a top-level attr instead of leaving them buried\n // in `longtask.attribution.container_type`.\n const containerType =\n info.longtaskEntry.attribution[0]?.containerType;\n if (containerType) {\n span.setAttribute(\"longtask.container_type\", containerType);\n }\n },\n }),\n ],\n });\n}\n"],"mappings":"6pBAkBA,MAAM,gBAAkB,aAClB,cAAgB,WAChB,cAAgB,WAShB,yBACJ,qDAWI,mBAA+B,CACnC,oBACA,YACA,aACA,eACA,cACA,0BACA,0BACA,oBACA,qBACA,kBACA,eACA,2BACA,yBACA,WACF,EAEM,YAAc,KACd,UAAY,sBAElB,SAAS,mBAA4B,CACnC,GAAI,OAAO,OAAW,IACpB,OAAO,YAET,GAAM,CAAE,SAAU,MAAS,OAAO,SAClC,OAAW,OAAO,IAAI,SAAS,IAAI,KAAK,QAAQ,UAAW,MAAM,GAAG,CACtE,CAEA,SAAS,sBACP,UACA,QACA,KACM,CACN,IAAM,KAAO,mBAAmB,OAAO,EACvC,KAAK,aAAa,gBAAiB,SAAS,EAC5C,KAAK,aAAa,gBAAiB,KAAK,GAAG,EACvC,KAAK,IACP,KAAK,aAAa,eAAgB,KAAK,EAAE,EAEvC,KAAK,MACP,KAAK,aAAa,iBAAkB,KAAK,IAAI,EAE3C,KAAK,MACP,KAAK,aAAa,iBAAkB,KAAK,IAAI,EAE3C,KAAK,WACP,KAAK,aAAa,uBAAwB,KAAK,SAAS,EAItD,KAAK,MACP,KAAK,aAAa,iBAAkB,KAAK,IAAI,CAEjD,CAEA,SAAS,cACP,KACA,aACM,CACN,GAAI,OAAO,OAAW,IACpB,OAEF,IAAM,SAAW,OAAO,SAAS,SAC3B,MAAQ,eAAe,QAAQ,EACrC,KAAK,aAAa,cAAe,OAAS,QAAQ,EAClD,KAAK,aAAa,cAAe,OAAO,SAAS,IAAI,EACjD,OACF,KAAK,aAAa,gBAAiB,KAAK,CAE5C,CAuCA,SAAgB,gCACd,MACY,CACZ,GAAM,CAAE,eAAgB,WAAY,qBAAuB,CAAC,GAAM,MAC5D,aAAe,MAAM,aACrB,OAAS,kBAAkB,EAC3B,YAAc,CAAC,GAAG,WAAY,GAAG,kBAAkB,EAEzD,OAAO,yBAAyB,CAC9B,eACA,iBAAkB,CAChB,IAAI,qBAAqB,CACvB,6BAA8B,CAAC,OAAQ,GAAG,oBAAoB,EAC9D,WAAY,YACZ,sBAAuB,yBACvB,mBAAoB,GACpB,6BACE,KACA,UACG,CACH,IAAM,IACJ,mBAAmB,QAAU,QAAQ,IAAM,SAAS,SAAS,EAC3D,UAAO,KAAQ,SAGnB,GAAI,CACF,IAAM,OAAS,IAAI,IAAI,GAAG,EAO1B,GANA,KAAK,aAAa,cAAe,OAAO,QAAQ,EAE9C,OAAO,KAAK,GAAG,GACf,qBAAqB,KAAM,IACzB,cAAc,OAAS,GAAG,KAAK,GAAG,EAAI,IAAI,SAAS,EAAE,CACvD,EACa,CACb,IAAM,QACH,mBAAmB,QAAU,QAAQ,OAAS,IAAA,KAC/C,MACI,MAAQ,eAAe,OAAO,QAAQ,EACxC,OACF,KAAK,aAAa,gBAAiB,KAAK,EAE1C,KAAK,WAAW,GAAG,OAAO,GAAG,OAAS,OAAO,UAAU,CACzD,CACF,MAAQ,CACN,MACF,CACF,CACF,CAAC,EACD,IAAI,8BAA8B,CAChC,6BAA8B,CAAC,OAAQ,GAAG,oBAAoB,EAC9D,WAAY,YACZ,sBAAuB,wBACzB,CAAC,EACD,IAAI,4BAA4B,CAC9B,sBAAuB,yBAKvB,4BAA6B,CAC3B,eAAgB,KAAM,QAAU,CAC9B,IAAM,KAAO,OAAO,KAChB,UAAO,MAAS,SAGpB,GAAI,CACF,IAAM,IAAM,IAAI,IAAI,IAAI,EAClB,cACJ,OAAO,OAAW,KAClB,IAAI,SAAW,OAAO,SAAS,OACjC,KAAK,WACH,cACI,aAAa,IAAI,OAAO,IAAI,WAC5B,aAAa,IAAI,UACvB,EACA,KAAK,aAAa,cAAe,IAAI,EACrC,KAAK,aAAa,cAAe,IAAI,QAAQ,EAC7C,KAAK,aAAa,iBAAkB,IAAI,IAAI,CAC9C,MAAQ,CAIR,CACF,CACF,CACF,CAAC,EAMD,IAAI,iCAAiC,CACnC,YAAa,kBACf,CAAC,EACD,IAAI,+BAA+B,CACjC,WAAY,CAAC,QAAS,QAAQ,EAO9B,2BAA4B,UAAW,QAAS,OAC1C,YAAc,SAAW,CAAC,aAAa,OAAO,EACzC,IAET,sBAAsB,UAAW,QAAS,IAAI,EAC9C,cAAc,KAAM,YAAY,EACzB,GAEX,CAAC,EAMD,IAAI,wBAAwB,CAC1B,kBAAmB,KAAM,OAAsC,CAC7D,cAAc,KAAM,YAAY,EAOhC,IAAM,cACJ,KAAK,cAAc,YAAY,IAAI,cACjC,eACF,KAAK,aAAa,0BAA2B,aAAa,CAE9D,CACF,CAAC,CACH,CACF,CAAC,CACH"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context } from "@opentelemetry/api";
|
|
2
|
+
import { ZoneContextManager } from "@opentelemetry/context-zone";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/otel/page-scope-context-manager.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `ZoneContextManager` returns `ROOT_CONTEXT` whenever `context.active()` is
|
|
7
|
+
* called outside a zone the manager itself created — which in practice is
|
|
8
|
+
* most of the time:
|
|
9
|
+
*
|
|
10
|
+
* - app code calling `fetch(...)` from a `useEffect`
|
|
11
|
+
* - `PerformanceObserver` callbacks (long-task, paint timing, …)
|
|
12
|
+
* - top-level await / module init code
|
|
13
|
+
* - listeners attached before the SDK booted
|
|
14
|
+
*
|
|
15
|
+
* Spans created from those code paths default to "no parent" → root, which
|
|
16
|
+
* is why a single page load produces N disjoint traces (one per
|
|
17
|
+
* instrumentation) instead of one waterfall.
|
|
18
|
+
*
|
|
19
|
+
* This subclass adds a single fallback: if the underlying zone lookup yields
|
|
20
|
+
* `ROOT_CONTEXT`, return the page-scope context instead. The page-scope
|
|
21
|
+
* context is built once at SDK init from the `<meta name="traceparent">`
|
|
22
|
+
* tag emitted by the SSR layout. If the meta tag is absent the page scope
|
|
23
|
+
* stays at `ROOT_CONTEXT` and the manager behaves like a stock
|
|
24
|
+
* `ZoneContextManager`.
|
|
25
|
+
*/
|
|
26
|
+
declare class PageScopeContextManager extends ZoneContextManager {
|
|
27
|
+
private pageScope;
|
|
28
|
+
setPageScope(ctx: Context): void;
|
|
29
|
+
active(): Context;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { PageScopeContextManager };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-scope-context-manager.d.mts","names":[],"sources":["../../../src/internal/otel/page-scope-context-manager.ts"],"mappings":";;;;;;AAyBA;;;;;;;;;;;;;;;;AAO4B;;;cAPf,uBAAA,SAAgC,kBAAA;EAAA,QACnC,SAAA;EAER,YAAA,CAAa,GAAA,EAAK,OAAA;EAIT,MAAA,CAAA,GAAU,OAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ROOT_CONTEXT}from"@opentelemetry/api";import{ZoneContextManager}from"@opentelemetry/context-zone";var PageScopeContextManager=class extends ZoneContextManager{pageScope=ROOT_CONTEXT;setPageScope(ctx){this.pageScope=ctx}active(){let ctx=super.active();return ctx===ROOT_CONTEXT?this.pageScope:ctx}};export{PageScopeContextManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-scope-context-manager.mjs","names":[],"sources":["../../../src/internal/otel/page-scope-context-manager.ts"],"sourcesContent":["import type { Context } from \"@opentelemetry/api\";\nimport { ROOT_CONTEXT } from \"@opentelemetry/api\";\nimport { ZoneContextManager } from \"@opentelemetry/context-zone\";\n\n/**\n * `ZoneContextManager` returns `ROOT_CONTEXT` whenever `context.active()` is\n * called outside a zone the manager itself created — which in practice is\n * most of the time:\n *\n * - app code calling `fetch(...)` from a `useEffect`\n * - `PerformanceObserver` callbacks (long-task, paint timing, …)\n * - top-level await / module init code\n * - listeners attached before the SDK booted\n *\n * Spans created from those code paths default to \"no parent\" → root, which\n * is why a single page load produces N disjoint traces (one per\n * instrumentation) instead of one waterfall.\n *\n * This subclass adds a single fallback: if the underlying zone lookup yields\n * `ROOT_CONTEXT`, return the page-scope context instead. The page-scope\n * context is built once at SDK init from the `<meta name=\"traceparent\">`\n * tag emitted by the SSR layout. If the meta tag is absent the page scope\n * stays at `ROOT_CONTEXT` and the manager behaves like a stock\n * `ZoneContextManager`.\n */\nexport class PageScopeContextManager extends ZoneContextManager {\n private pageScope: Context = ROOT_CONTEXT;\n\n setPageScope(ctx: Context): void {\n this.pageScope = ctx;\n }\n\n override active(): Context {\n const ctx = super.active();\n return ctx === ROOT_CONTEXT ? this.pageScope : ctx;\n }\n}\n"],"mappings":"yGAyBA,IAAa,wBAAb,cAA6C,kBAAmB,CAC9D,UAA6B,aAE7B,aAAa,IAAoB,CAC/B,KAAK,UAAY,GACnB,CAEA,QAA2B,CACzB,IAAM,IAAM,MAAM,OAAO,EACzB,OAAO,MAAQ,aAAe,KAAK,UAAY,GACjD,CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/otel/propagation.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reads the W3C `traceparent` (and optional `tracestate`) meta tag from the
|
|
6
|
+
* document head and extracts an OTel `Context`. SSR renderers (Next, Vite,
|
|
7
|
+
* …) inject the server-side request span's context so the client SDK can
|
|
8
|
+
* stitch its spans onto the same trace.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when:
|
|
11
|
+
* - called server-side (no `document`)
|
|
12
|
+
* - no `traceparent` meta tag is present
|
|
13
|
+
* - the extracted context has an invalid trace id
|
|
14
|
+
* - the `sampled` flag is unset — the browser doesn't honor the bit when
|
|
15
|
+
* creating child spans, so an unsampled parent would orphan every
|
|
16
|
+
* browser span under a trace with no recorded server segments. Drop
|
|
17
|
+
* instead so spans fall back to a fresh root.
|
|
18
|
+
*/
|
|
19
|
+
declare function readPropagationFromDocument(): Context | null;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { readPropagationFromDocument };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propagation.d.mts","names":[],"sources":["../../../src/internal/otel/propagation.ts"],"mappings":";;;;;AAuCA;;;;AAAsD;;;;;;;;;iBAAtC,2BAAA,CAAA,GAA+B,OAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ROOT_CONTEXT,defaultTextMapGetter,trace}from"@opentelemetry/api";import{W3CTraceContextPropagator}from"@opentelemetry/core";const TRACESTATE_META=`tracestate`,W3C_PROPAGATOR=new W3CTraceContextPropagator;function readMeta(name){return typeof document>`u`?null:document.querySelector(`meta[name="${name}"]`)?.getAttribute(`content`)??null}function readPropagationFromDocument(){let traceparent=readMeta(`traceparent`);if(!traceparent)return null;let carrier={traceparent},tracestate=readMeta(TRACESTATE_META);tracestate&&(carrier[TRACESTATE_META]=tracestate);let extracted=W3C_PROPAGATOR.extract(ROOT_CONTEXT,carrier,defaultTextMapGetter),spanCtx=trace.getSpanContext(extracted);return!spanCtx||spanCtx.traceId===`00000000000000000000000000000000`||!(spanCtx.traceFlags&1)?null:extracted}export{readPropagationFromDocument};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propagation.mjs","names":[],"sources":["../../../src/internal/otel/propagation.ts"],"sourcesContent":["import {\n type Context,\n defaultTextMapGetter,\n ROOT_CONTEXT,\n trace,\n} from \"@opentelemetry/api\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\n\nconst TRACEPARENT_META = \"traceparent\";\nconst TRACESTATE_META = \"tracestate\";\nconst INVALID_TRACE_ID = \"00000000000000000000000000000000\";\nconst TRACE_FLAGS_SAMPLED = 0x1;\n// Local instance — extraction must not depend on whether the customer (or\n// our own bootstrap) set a global propagator.\nconst W3C_PROPAGATOR = new W3CTraceContextPropagator();\n\nfunction readMeta(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const el = document.querySelector(`meta[name=\"${name}\"]`);\n return el?.getAttribute(\"content\") ?? null;\n}\n\n/**\n * Reads the W3C `traceparent` (and optional `tracestate`) meta tag from the\n * document head and extracts an OTel `Context`. SSR renderers (Next, Vite,\n * …) inject the server-side request span's context so the client SDK can\n * stitch its spans onto the same trace.\n *\n * Returns `null` when:\n * - called server-side (no `document`)\n * - no `traceparent` meta tag is present\n * - the extracted context has an invalid trace id\n * - the `sampled` flag is unset — the browser doesn't honor the bit when\n * creating child spans, so an unsampled parent would orphan every\n * browser span under a trace with no recorded server segments. Drop\n * instead so spans fall back to a fresh root.\n */\nexport function readPropagationFromDocument(): Context | null {\n const traceparent = readMeta(TRACEPARENT_META);\n if (!traceparent) {\n return null;\n }\n\n const carrier: Record<string, string> = { traceparent };\n const tracestate = readMeta(TRACESTATE_META);\n if (tracestate) {\n carrier[TRACESTATE_META] = tracestate;\n }\n\n const extracted = W3C_PROPAGATOR.extract(\n ROOT_CONTEXT,\n carrier,\n defaultTextMapGetter\n );\n const spanCtx = trace.getSpanContext(extracted);\n // octet of flag bits per the W3C trace-context spec; bit 0 is the sampled\n // bit. Bitwise AND is the canonical extraction.\n if (\n !spanCtx ||\n spanCtx.traceId === INVALID_TRACE_ID ||\n // biome-ignore lint/suspicious/noBitwiseOperators: traceFlags is a single\n (spanCtx.traceFlags & TRACE_FLAGS_SAMPLED) === 0\n ) {\n return null;\n }\n return extracted;\n}\n"],"mappings":"mIAQA,MACM,gBAAkB,aAKlB,eAAiB,IAAI,0BAE3B,SAAS,SAAS,KAA6B,CAK7C,OAJI,OAAO,SAAa,IACf,KAEE,SAAS,cAAc,cAAc,KAAK,GAC7C,GAAG,aAAa,SAAS,GAAK,IACxC,CAiBA,SAAgB,6BAA8C,CAC5D,IAAM,YAAc,SAAS,aAAgB,EAC7C,GAAI,CAAC,YACH,OAAO,KAGT,IAAM,QAAkC,CAAE,WAAY,EAChD,WAAa,SAAS,eAAe,EACvC,aACF,QAAQ,iBAAmB,YAG7B,IAAM,UAAY,eAAe,QAC/B,aACA,QACA,oBACF,EACM,QAAU,MAAM,eAAe,SAAS,EAW9C,MAPE,CAAC,SACD,QAAQ,UAAY,oCAAA,EAEnB,QAAQ,WAAa,GAEf,KAEF,SACT"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { PageScopeContextManager } from "./page-scope-context-manager.mjs";
|
|
2
|
+
import { ReleaseSlug } from "@interfere/types/releases/slug";
|
|
3
|
+
import { MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
4
|
+
import { LogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
5
|
+
import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
6
|
+
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
7
|
+
import { SessionId } from "@interfere/types/data/session";
|
|
8
|
+
|
|
9
|
+
//#region src/internal/otel/provider.d.ts
|
|
10
|
+
interface OtelProviderInput {
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
*
|
|
14
|
+
* Extra log-record processors fanned into the LoggerProvider's
|
|
15
|
+
* processor list at construction. Used by `@interfere/observability`
|
|
16
|
+
* for internal-only dual-write to BetterStack — keeps a parallel
|
|
17
|
+
* destination's queue/exporter independent of the SDK's own.
|
|
18
|
+
* Customers don't get a fan-out hook on the SDK surface; this is a
|
|
19
|
+
* private bridge for our own dogfood apps.
|
|
20
|
+
*/
|
|
21
|
+
additionalLogRecordProcessors?: LogRecordProcessor[];
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
*
|
|
25
|
+
* Extra metric readers fanned into the MeterProvider's reader list
|
|
26
|
+
* at construction. Used by `@interfere/observability` for internal-
|
|
27
|
+
* only dual-write of web vitals histograms (`web_vitals.{ttfb,fcp,
|
|
28
|
+
* lcp,inp,cls}`) to the BetterStack-fronting OTel collector. Each
|
|
29
|
+
* reader owns its own queue + exporter so a 5xx / network outage on
|
|
30
|
+
* the parallel destination backs off only its own batch.
|
|
31
|
+
*/
|
|
32
|
+
additionalMetricReaders?: MetricReader[];
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*
|
|
36
|
+
* Extra span processors fanned into the WebTracerProvider's processor
|
|
37
|
+
* list at construction. See `additionalLogRecordProcessors`.
|
|
38
|
+
*/
|
|
39
|
+
additionalSpanProcessors?: SpanProcessor[];
|
|
40
|
+
/**
|
|
41
|
+
* Auth + content headers for OTLP exports. Sourced from the kernel's
|
|
42
|
+
* already-resolved ingest target so the OTLP path inherits the same
|
|
43
|
+
* transport metadata the rest of the SDK uses — no separate auth knob.
|
|
44
|
+
*/
|
|
45
|
+
authHeaders: Headers;
|
|
46
|
+
/** OTLP base URL — the opaque sink path is appended by the exporters. */
|
|
47
|
+
collectorUrl: string;
|
|
48
|
+
/**
|
|
49
|
+
* Stable per-device identifier from `DeviceManager`. Resolved from
|
|
50
|
+
* localStorage / cookie at kernel boot, so it's synchronously
|
|
51
|
+
* available before the providers are constructed. Stamped as the
|
|
52
|
+
* OTel-semconv `device.id` resource attribute so every span / log /
|
|
53
|
+
* metric carries it without per-record re-stamping. Optional —
|
|
54
|
+
* tests and SSR boot paths that build the provider before the
|
|
55
|
+
* device manager has run pass `null`.
|
|
56
|
+
*/
|
|
57
|
+
deviceId?: string | null | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the kernel's current session id. Wired into per-span /
|
|
60
|
+
* per-log session processors so a mid-page session rotation (30-min
|
|
61
|
+
* idle expiry, manual reset) lands on subsequent spans without the
|
|
62
|
+
* kernel having to rebuild the providers. Returns `null` before the
|
|
63
|
+
* `SessionTracker` has bootstrapped.
|
|
64
|
+
*/
|
|
65
|
+
getSessionId: () => SessionId | null;
|
|
66
|
+
/** Optional release slug — top-level `release.slug` attr (Phase 6 wire identity). */
|
|
67
|
+
releaseSlug?: ReleaseSlug | null | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Producer chain for the `interfere.sdk.stack` resource attribute —
|
|
70
|
+
* e.g. `["@interfere/next@10.0.0", "@interfere/react@10.0.0"]`.
|
|
71
|
+
* Wrapper-injected; the kernel just forwards.
|
|
72
|
+
*/
|
|
73
|
+
sdkStack: string[];
|
|
74
|
+
/**
|
|
75
|
+
* Override the OTel `service.name` resource attribute. Defaults to
|
|
76
|
+
* `"interfere-sdk"`.
|
|
77
|
+
*/
|
|
78
|
+
serviceName?: string;
|
|
79
|
+
}
|
|
80
|
+
interface OtelProviderHandle {
|
|
81
|
+
/**
|
|
82
|
+
* Page-scope context manager. The kernel calls `setPageScope(ctx)` after
|
|
83
|
+
* extracting the SSR `traceparent` meta tag so spans without a more
|
|
84
|
+
* specific zone descend from the server-side parent.
|
|
85
|
+
*/
|
|
86
|
+
contextManager: PageScopeContextManager;
|
|
87
|
+
/** Force-flush all three providers — call from kernel.flush() / on visibility hidden. */
|
|
88
|
+
flush(): Promise<void>;
|
|
89
|
+
loggerProvider: LoggerProvider;
|
|
90
|
+
meterProvider: MeterProvider;
|
|
91
|
+
metricReader: MetricReader;
|
|
92
|
+
/** Tear down all three providers — called from kernel.dispose(). */
|
|
93
|
+
shutdown(): Promise<void>;
|
|
94
|
+
tracerProvider: WebTracerProvider;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Constructs the SDK's **private** OTel providers — never registers globally
|
|
98
|
+
* via `provider.register()` or `trace.setGlobalTracerProvider()`. Customers
|
|
99
|
+
* with their own OTel setup (DataDog, Vercel, etc.) cohabit cleanly.
|
|
100
|
+
*
|
|
101
|
+
* Splitting this out from `Kernel` keeps the OTel module lazy-loadable so
|
|
102
|
+
* the error-only bundle path (no tracing) never imports the OTel SDK.
|
|
103
|
+
*/
|
|
104
|
+
declare function buildOtelProvider(input: OtelProviderInput): OtelProviderHandle;
|
|
105
|
+
//#endregion
|
|
106
|
+
export { OtelProviderHandle, OtelProviderInput, buildOtelProvider };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.mts","names":[],"sources":["../../../src/internal/otel/provider.ts"],"mappings":";;;;;;;;;UA0GiB,iBAAA;;AAAjB;;;;;;;;;EAWE,6BAAA,GAAgC,kBAAA;EA8CP;;;;;;;;;;EAnCzB,uBAAA,GAA0B,YAAA;EAiC1B;;;;;;EA1BA,wBAAA,GAA2B,aAAA;EAuChB;AAGb;;;;EApCE,WAAA,EAAa,OAAA;EA6CG;EA3ChB,YAAA;EA6Cc;;;;;;;;;EAnCd,QAAA;EAiCgB;;;;;;;EAzBhB,YAAA,QAAoB,SAAA;EA8BJ;EA5BhB,WAAA,GAAc,WAAA;EA4BmB;AAWnC;;;;EAjCE,QAAA;EAkCA;;;AACmB;EA9BnB,WAAA;AAAA;AAAA,UAGe,kBAAA;;;;;;EAMf,cAAA,EAAgB,uBAAA;;EAEhB,KAAA,IAAS,OAAA;EACT,cAAA,EAAgB,cAAA;EAChB,aAAA,EAAe,aAAA;EACf,YAAA,EAAc,YAAA;;EAEd,QAAA,IAAY,OAAA;EACZ,cAAA,EAAgB,iBAAA;AAAA;;;;;;;;;iBAWF,iBAAA,CACd,KAAA,EAAO,iBAAA,GACN,kBAAkB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SDK_NAME,SDK_VERSION}from"../version.mjs";import{createBeaconLogExporter,createBeaconMetricExporter,createBeaconTraceExporter}from"./exporter.mjs";import{PageScopeContextManager}from"./page-scope-context-manager.mjs";import{propagation}from"@opentelemetry/api";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{normalizeEnv}from"@interfere/types/sdk/runtime";import{browserDetector}from"@opentelemetry/opentelemetry-browser-detector";import{detectResources,resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{WebTracerProvider}from"@opentelemetry/sdk-trace-web";import{createSessionLogRecordProcessor,createSessionSpanProcessor}from"@opentelemetry/web-common";const BROWSER_BATCH_OPTIONS={maxQueueSize:50,maxExportBatchSize:10,scheduledDelayMillis:2e3,exportTimeoutMillis:1e4};function buildOtelProvider(input){let environment=normalizeEnv(typeof process>`u`?void 0:process.env.NODE_ENV)??`unknown`,baseResource=resourceFromAttributes({"service.name":input.serviceName??`interfere-sdk`,"service.namespace":`interfere`,"deployment.environment.name":environment,"telemetry.sdk.language":`webjs`,"interfere.sdk.name":SDK_NAME,"interfere.sdk.version":SDK_VERSION,"interfere.sdk.stack":input.sdkStack.join(`, `),...input.releaseSlug?{"release.slug":input.releaseSlug}:{},...input.deviceId?{"device.id":input.deviceId}:{}}),detected=detectResources({detectors:[browserDetector]}),resource=baseResource.merge(detected),traceExporter=createBeaconTraceExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),getSessionId=input.getSessionId,tracerProvider=new WebTracerProvider({resource,spanProcessors:[createSessionSpanProcessor({getSessionId}),new BatchSpanProcessor(traceExporter,BROWSER_BATCH_OPTIONS),...input.additionalSpanProcessors??[]]}),contextManager=new PageScopeContextManager;tracerProvider.register({contextManager}),propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new W3CBaggagePropagator]}));let metricReader=new PeriodicExportingMetricReader({exporter:createBeaconMetricExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),exportIntervalMillis:3e4}),meterProvider=new MeterProvider({resource,readers:[metricReader,...input.additionalMetricReaders??[]]}),logExporter=createBeaconLogExporter({collectorUrl:input.collectorUrl,authHeaders:input.authHeaders}),loggerProvider=new LoggerProvider({resource,processors:[createSessionLogRecordProcessor({getSessionId}),new BatchLogRecordProcessor(logExporter,BROWSER_BATCH_OPTIONS),...input.additionalLogRecordProcessors??[]]});return{contextManager,tracerProvider,meterProvider,metricReader,loggerProvider,flush:async()=>{await Promise.all([tracerProvider.forceFlush(),metricReader.forceFlush(),loggerProvider.forceFlush()])},shutdown:async()=>{await Promise.all([tracerProvider.shutdown(),meterProvider.shutdown(),loggerProvider.shutdown()])}}}export{buildOtelProvider};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.mjs","names":[],"sources":["../../../src/internal/otel/provider.ts"],"sourcesContent":["import type { SessionId } from \"@interfere/types/data/session\";\nimport type { ReleaseSlug } from \"@interfere/types/releases/slug\";\nimport { normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nimport { propagation } from \"@opentelemetry/api\";\nimport {\n CompositePropagator,\n W3CBaggagePropagator,\n W3CTraceContextPropagator,\n} from \"@opentelemetry/core\";\nimport { browserDetector } from \"@opentelemetry/opentelemetry-browser-detector\";\nimport {\n detectResources,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n type LogRecordProcessor,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n MeterProvider,\n type MetricReader,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { WebTracerProvider } from \"@opentelemetry/sdk-trace-web\";\nimport {\n createSessionLogRecordProcessor,\n createSessionSpanProcessor,\n} from \"@opentelemetry/web-common\";\n\nimport { SDK_NAME, SDK_VERSION } from \"../version.js\";\nimport {\n createBeaconLogExporter,\n createBeaconMetricExporter,\n createBeaconTraceExporter,\n} from \"./exporter.js\";\nimport { PageScopeContextManager } from \"./page-scope-context-manager.js\";\n\nconst SERVICE_NAMESPACE = \"interfere\";\n\n// Inlined wire keys — matches the pattern in\n// `@interfere/observability/browser/rum` (semconv keeps shifting these\n// between minor versions, but the wire keys themselves are stable).\nconst ATTR_SERVICE_NAME = \"service.name\" as const;\nconst ATTR_SERVICE_NAMESPACE = \"service.namespace\" as const;\nconst ATTR_DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\" as const;\nconst ATTR_TELEMETRY_SDK_LANGUAGE = \"telemetry.sdk.language\" as const;\nconst ATTR_RELEASE_SLUG = \"release.slug\" as const;\nconst ATTR_INTERFERE_SDK_STACK = \"interfere.sdk.stack\" as const;\nconst ATTR_INTERFERE_SDK_NAME = \"interfere.sdk.name\" as const;\nconst ATTR_INTERFERE_SDK_VERSION = \"interfere.sdk.version\" as const;\n// OTel semconv-canonical name for a stable per-device identifier.\n// Stamped as a *resource* attribute (not per-span) because it's stable\n// for the lifetime of the page; sessions rotate, devices don't. The\n// collector reads this off `context.resourceAttributes[\"device.id\"]`\n// and projects it into `spans.deviceId`.\nconst ATTR_DEVICE_ID = \"device.id\" as const;\n\nconst DEFAULT_SERVICE_NAME = \"interfere-sdk\";\n\n/**\n * Tuned for `navigator.sendBeacon`'s per-call payload ceiling.\n *\n * The browser primary exporters dispatch every export via beacon\n * (see `BeaconTraceExporter` / `BeaconLogExporter` /\n * `BeaconMetricExporter` in `exporter.ts`) — sendBeacon is the only\n * browser transport that reliably commits a request the page is also\n * tearing down. It's not free though: most browsers cap each beacon\n * payload at 64KiB (Chrome / Firefox) and reject anything larger\n * with a `false` return from `navigator.sendBeacon` — at which point\n * the BSP just drops the batch (no retry: the SW backstop intercepts\n * 5xx / network failures, not user-agent rejections).\n *\n * The numbers below keep payloads comfortably under that ceiling,\n * even on jank-heavy pages (production data on 2026-05-11 saw 831\n * `longtask` spans piled up on a single page-hide, observed via the\n * BetterStack dual-write) where a 100-span batch would already top\n * 100KiB:\n *\n * - `maxExportBatchSize: 10` — caps each beacon at ~10–20KiB,\n * leaving headroom for span attribute bloat without flirting\n * with the per-call ceiling.\n * - `maxQueueSize: 50` — caps the worst-case unload payload at\n * ~50–100KiB; the BSP drains across 5 beacons when forced-\n * flushed on `visibilitychange→hidden`, all of which queue\n * synchronously inside the user agent.\n * - `scheduledDelayMillis: 2000` — quieter pages keep less in\n * buffer at any moment, so a hidden→visible→hidden bounce\n * doesn't accumulate 5s of telemetry waiting to ship.\n * - `exportTimeoutMillis: 10_000` — unchanged. Beacon dispatches\n * are essentially synchronous (the user agent enqueues into its\n * own send loop), so the timeout is mostly defensive against\n * pathological `JsonTraceSerializer.serializeRequest` calls.\n */\nconst BROWSER_BATCH_OPTIONS = {\n maxQueueSize: 50,\n maxExportBatchSize: 10,\n scheduledDelayMillis: 2000,\n exportTimeoutMillis: 10_000,\n} as const;\n\nexport interface OtelProviderInput {\n /**\n * @internal\n *\n * Extra log-record processors fanned into the LoggerProvider's\n * processor list at construction. Used by `@interfere/observability`\n * for internal-only dual-write to BetterStack — keeps a parallel\n * destination's queue/exporter independent of the SDK's own.\n * Customers don't get a fan-out hook on the SDK surface; this is a\n * private bridge for our own dogfood apps.\n */\n additionalLogRecordProcessors?: LogRecordProcessor[];\n /**\n * @internal\n *\n * Extra metric readers fanned into the MeterProvider's reader list\n * at construction. Used by `@interfere/observability` for internal-\n * only dual-write of web vitals histograms (`web_vitals.{ttfb,fcp,\n * lcp,inp,cls}`) to the BetterStack-fronting OTel collector. Each\n * reader owns its own queue + exporter so a 5xx / network outage on\n * the parallel destination backs off only its own batch.\n */\n additionalMetricReaders?: MetricReader[];\n /**\n * @internal\n *\n * Extra span processors fanned into the WebTracerProvider's processor\n * list at construction. See `additionalLogRecordProcessors`.\n */\n additionalSpanProcessors?: SpanProcessor[];\n /**\n * Auth + content headers for OTLP exports. Sourced from the kernel's\n * already-resolved ingest target so the OTLP path inherits the same\n * transport metadata the rest of the SDK uses — no separate auth knob.\n */\n authHeaders: Headers;\n /** OTLP base URL — the opaque sink path is appended by the exporters. */\n collectorUrl: string;\n /**\n * Stable per-device identifier from `DeviceManager`. Resolved from\n * localStorage / cookie at kernel boot, so it's synchronously\n * available before the providers are constructed. Stamped as the\n * OTel-semconv `device.id` resource attribute so every span / log /\n * metric carries it without per-record re-stamping. Optional —\n * tests and SSR boot paths that build the provider before the\n * device manager has run pass `null`.\n */\n deviceId?: string | null | undefined;\n /**\n * Returns the kernel's current session id. Wired into per-span /\n * per-log session processors so a mid-page session rotation (30-min\n * idle expiry, manual reset) lands on subsequent spans without the\n * kernel having to rebuild the providers. Returns `null` before the\n * `SessionTracker` has bootstrapped.\n */\n getSessionId: () => SessionId | null;\n /** Optional release slug — top-level `release.slug` attr (Phase 6 wire identity). */\n releaseSlug?: ReleaseSlug | null | undefined;\n /**\n * Producer chain for the `interfere.sdk.stack` resource attribute —\n * e.g. `[\"@interfere/next@10.0.0\", \"@interfere/react@10.0.0\"]`.\n * Wrapper-injected; the kernel just forwards.\n */\n sdkStack: string[];\n /**\n * Override the OTel `service.name` resource attribute. Defaults to\n * `\"interfere-sdk\"`.\n */\n serviceName?: string;\n}\n\nexport interface OtelProviderHandle {\n /**\n * Page-scope context manager. The kernel calls `setPageScope(ctx)` after\n * extracting the SSR `traceparent` meta tag so spans without a more\n * specific zone descend from the server-side parent.\n */\n contextManager: PageScopeContextManager;\n /** Force-flush all three providers — call from kernel.flush() / on visibility hidden. */\n flush(): Promise<void>;\n loggerProvider: LoggerProvider;\n meterProvider: MeterProvider;\n metricReader: MetricReader;\n /** Tear down all three providers — called from kernel.dispose(). */\n shutdown(): Promise<void>;\n tracerProvider: WebTracerProvider;\n}\n\n/**\n * Constructs the SDK's **private** OTel providers — never registers globally\n * via `provider.register()` or `trace.setGlobalTracerProvider()`. Customers\n * with their own OTel setup (DataDog, Vercel, etc.) cohabit cleanly.\n *\n * Splitting this out from `Kernel` keeps the OTel module lazy-loadable so\n * the error-only bundle path (no tracing) never imports the OTel SDK.\n */\nexport function buildOtelProvider(\n input: OtelProviderInput\n): OtelProviderHandle {\n // Static service identity merged with detected browser attrs\n // (`browser.brands`, `browser.platform`, `browser.mobile`,\n // `browser.language`, `browser.user_agent`). The browser detector\n // reads UA Client Hints synchronously via `navigator.userAgentData`\n // low-entropy properties, so this stays sync — async would race\n // `DocumentLoadInstrumentation` and consistently miss page-load.\n const environment =\n normalizeEnv(\n typeof process === \"undefined\" ? undefined : process.env[\"NODE_ENV\"]\n ) ?? \"unknown\";\n\n const baseResource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: input.serviceName ?? DEFAULT_SERVICE_NAME,\n [ATTR_SERVICE_NAMESPACE]: SERVICE_NAMESPACE,\n [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: environment,\n [ATTR_TELEMETRY_SDK_LANGUAGE]: \"webjs\",\n [ATTR_INTERFERE_SDK_NAME]: SDK_NAME,\n [ATTR_INTERFERE_SDK_VERSION]: SDK_VERSION,\n [ATTR_INTERFERE_SDK_STACK]: input.sdkStack.join(\", \"),\n ...(input.releaseSlug ? { [ATTR_RELEASE_SLUG]: input.releaseSlug } : {}),\n ...(input.deviceId ? { [ATTR_DEVICE_ID]: input.deviceId } : {}),\n });\n const detected = detectResources({ detectors: [browserDetector] });\n const resource = baseResource.merge(detected);\n\n const traceExporter = createBeaconTraceExporter({\n collectorUrl: input.collectorUrl,\n authHeaders: input.authHeaders,\n });\n\n // The web-common session processor expects `() => string | null`,\n // matching our `KernelSession.getId` shape.\n const getSessionId = input.getSessionId;\n\n const spanProcessors: SpanProcessor[] = [\n // Stamps `session.id` per-span, re-reading on every export. A\n // mid-page session rotation (30-min idle, manual reset) lands on\n // subsequent spans without rebuilding the provider. Resource-level\n // `session.id` would freeze at boot.\n createSessionSpanProcessor({ getSessionId }),\n // Exception attribute defaults (`interfere.exception.{mechanism,\n // handled,kind}`) are no longer stamped here — third-party\n // auto-instrumentations bypass our kernel anyway, so the SDK\n // can't reliably tell what to default. Server-side enrichment\n // (`enrichment/lib/normalize-atom.ts`) is the single chokepoint\n // every exception event flows through, so the defaults live\n // there now and the SDK ships less code.\n new BatchSpanProcessor(traceExporter, BROWSER_BATCH_OPTIONS),\n // Internal-only fan-out (BetterStack dual-write for `interfere/homepage`\n // + `interfere/dashboard`). Each extra processor owns its own queue +\n // exporter, so a 5xx / network outage on the parallel destination\n // backs off only its own batch — the primary collector pipeline keeps\n // draining unaffected.\n ...(input.additionalSpanProcessors ?? []),\n ];\n\n const tracerProvider = new WebTracerProvider({ resource, spanProcessors });\n\n // `PageScopeContextManager` keeps the active context alive across\n // microtasks/promise chains/setTimeout, AND falls back to the page-scope\n // context (the SSR `traceparent`) for code paths that fire outside any\n // zone the manager itself created. The kernel populates the page scope\n // by calling `contextManager.setPageScope(ctx)` after extracting the\n // meta tag.\n //\n // `register({ contextManager })` does call `setGlobalContextManager`\n // internally — that's unavoidable; OTel exposes exactly one global\n // context manager. Customers with their own OTel provider boot last and\n // win. We accept the trade-off for correctness: the alternative\n // (separate per-provider context manager) is not supported by OTel.\n const contextManager = new PageScopeContextManager();\n tracerProvider.register({ contextManager });\n\n // Composed W3C trace context + baggage propagator. `FetchInstrumentation`\n // injects both `traceparent` and `baggage` on outgoing requests so\n // backend Elysia plugins can pick `interfere.*` baggage entries off\n // the request and stamp them on server spans. Only set the global if\n // no propagator is currently registered (`fields()` is `[]` for the\n // noop). Customers who installed their own propagator (B3, composite,\n // …) are preserved. Reading the SSR meta tag uses a local instance,\n // not the global, so our extract path is independent.\n if (propagation.fields().length === 0) {\n propagation.setGlobalPropagator(\n new CompositePropagator({\n propagators: [\n new W3CTraceContextPropagator(),\n new W3CBaggagePropagator(),\n ],\n })\n );\n }\n\n const metricExporter = createBeaconMetricExporter({\n collectorUrl: input.collectorUrl,\n authHeaders: input.authHeaders,\n });\n\n const metricReader = new PeriodicExportingMetricReader({\n exporter: metricExporter,\n exportIntervalMillis: 30_000,\n });\n\n const meterProvider = new MeterProvider({\n resource,\n readers: [metricReader, ...(input.additionalMetricReaders ?? [])],\n });\n\n // Logs path. The `plugins/logs.ts` plugin patches `console.*` and emits\n // a `LogRecord` per call (errorsPlugin still owns Error-bearing console\n // calls — the class boundary is checked there). LoggerProvider is\n // private; the kernel exposes `recordLog` so callers don't have to\n // touch the OTel logs API directly.\n const logExporter = createBeaconLogExporter({\n collectorUrl: input.collectorUrl,\n authHeaders: input.authHeaders,\n });\n const loggerProvider = new LoggerProvider({\n resource,\n processors: [\n // Mirrors the SessionSpanProcessor for traces — every log record\n // (including `BrowserNavigationInstrumentation`'s navigation\n // events) carries the same `session.id` as the spans emitted\n // around it, so trace ↔ log correlation by session is trivial.\n createSessionLogRecordProcessor({ getSessionId }),\n new BatchLogRecordProcessor(logExporter, BROWSER_BATCH_OPTIONS),\n ...(input.additionalLogRecordProcessors ?? []),\n ],\n });\n\n return {\n contextManager,\n tracerProvider,\n meterProvider,\n metricReader,\n loggerProvider,\n flush: async () => {\n await Promise.all([\n tracerProvider.forceFlush(),\n metricReader.forceFlush(),\n loggerProvider.forceFlush(),\n ]);\n },\n shutdown: async () => {\n await Promise.all([\n tracerProvider.shutdown(),\n meterProvider.shutdown(),\n loggerProvider.shutdown(),\n ]);\n },\n };\n}\n"],"mappings":"27BA2CA,MAwDM,sBAAwB,CAC5B,aAAc,GACd,mBAAoB,GACpB,qBAAsB,IACtB,oBAAqB,GACvB,EAkGA,SAAgB,kBACd,MACoB,CAOpB,IAAM,YACJ,aACE,OAAO,QAAY,IAAc,IAAA,GAAY,QAAQ,IAAI,QAC3D,GAAK,UAED,aAAe,uBAAuB,CACzC,eAAoB,MAAM,aAAe,gBACzC,oBAAyB,YACzB,8BAAmC,YACnC,yBAA8B,QAC9B,qBAA0B,SAC1B,wBAA6B,YAC7B,sBAA2B,MAAM,SAAS,KAAK,IAAI,EACpD,GAAI,MAAM,YAAc,CAAG,eAAoB,MAAM,WAAY,EAAI,CAAC,EACtE,GAAI,MAAM,SAAW,CAAG,YAAiB,MAAM,QAAS,EAAI,CAAC,CAC/D,CAAC,EACK,SAAW,gBAAgB,CAAE,UAAW,CAAC,eAAe,CAAE,CAAC,EAC3D,SAAW,aAAa,MAAM,QAAQ,EAEtC,cAAgB,0BAA0B,CAC9C,aAAc,MAAM,aACpB,YAAa,MAAM,WACrB,CAAC,EAIK,aAAe,MAAM,aAwBrB,eAAiB,IAAI,kBAAkB,CAAE,SAAU,eAAA,CAjBvD,2BAA2B,CAAE,YAAa,CAAC,EAQ3C,IAAI,mBAAmB,cAAe,qBAAqB,EAM3D,GAAI,MAAM,0BAA4B,CAAC,CAG6B,CAAE,CAAC,EAcnE,eAAiB,IAAI,wBAC3B,eAAe,SAAS,CAAE,cAAe,CAAC,EAUtC,YAAY,OAAO,EAAE,SAAW,GAClC,YAAY,oBACV,IAAI,oBAAoB,CACtB,YAAa,CACX,IAAI,0BACJ,IAAI,oBACN,CACF,CAAC,CACH,EAQF,IAAM,aAAe,IAAI,8BAA8B,CACrD,SANqB,2BAA2B,CAChD,aAAc,MAAM,aACpB,YAAa,MAAM,WACrB,CAGyB,EACvB,qBAAsB,GACxB,CAAC,EAEK,cAAgB,IAAI,cAAc,CACtC,SACA,QAAS,CAAC,aAAc,GAAI,MAAM,yBAA2B,CAAC,CAAE,CAClE,CAAC,EAOK,YAAc,wBAAwB,CAC1C,aAAc,MAAM,aACpB,YAAa,MAAM,WACrB,CAAC,EACK,eAAiB,IAAI,eAAe,CACxC,SACA,WAAY,CAKV,gCAAgC,CAAE,YAAa,CAAC,EAChD,IAAI,wBAAwB,YAAa,qBAAqB,EAC9D,GAAI,MAAM,+BAAiC,CAAC,CAC9C,CACF,CAAC,EAED,MAAO,CACL,eACA,eACA,cACA,aACA,eACA,MAAO,SAAY,CACjB,MAAM,QAAQ,IAAI,CAChB,eAAe,WAAW,EAC1B,aAAa,WAAW,EACxB,eAAe,WAAW,CAC5B,CAAC,CACH,EACA,SAAU,SAAY,CACpB,MAAM,QAAQ,IAAI,CAChB,eAAe,SAAS,EACxB,cAAc,SAAS,EACvB,eAAe,SAAS,CAC1B,CAAC,CACH,CACF,CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Meter } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/otel/web-vitals.d.ts
|
|
4
|
+
interface WebVitalsInput {
|
|
5
|
+
/**
|
|
6
|
+
* Called after the page-load vitals have all fired. The kernel passes
|
|
7
|
+
* its own `flush()` so short-visit exports don't depend on the
|
|
8
|
+
* `visibilitychange` listener firing.
|
|
9
|
+
*/
|
|
10
|
+
flush: () => Promise<void>;
|
|
11
|
+
/** OTel meter from the kernel's private provider. */
|
|
12
|
+
meter: Meter;
|
|
13
|
+
/**
|
|
14
|
+
* Pathname → low-cardinality route template (e.g. `/blog/[slug]`).
|
|
15
|
+
* Without this, dynamic routes produce one unique `url.path` label
|
|
16
|
+
* per visited slug — a metric cardinality hazard.
|
|
17
|
+
*/
|
|
18
|
+
resolveRoute?: (pathname: string) => string | undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Subscribes to Core Web Vitals (LCP/FCP/TTFB/INP/CLS) and records OTel
|
|
22
|
+
* histograms. Time-based vitals are converted to seconds per
|
|
23
|
+
* observability convention; CLS is dimensionless and recorded as-is.
|
|
24
|
+
*
|
|
25
|
+
* Each record carries `web_vital.rating`, `web_vital.navigation_type`,
|
|
26
|
+
* and `url.path` (templated when `resolveRoute` is provided). Browser
|
|
27
|
+
* and device dimensions come from the resource attached to the
|
|
28
|
+
* MeterProvider, so every histogram datapoint already carries them.
|
|
29
|
+
*
|
|
30
|
+
* Bucket boundaries match the rum.ts SDK — same dashboards, same
|
|
31
|
+
* histograms, same percentile shape.
|
|
32
|
+
*/
|
|
33
|
+
declare function captureWebVitals(input: WebVitalsInput): void;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { WebVitalsInput, captureWebVitals };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-vitals.d.mts","names":[],"sources":["../../../src/internal/otel/web-vitals.ts"],"mappings":";;;UAmBiB,cAAA;;AAAjB;;;;EAME,KAAA,QAAa,OAAA;EAAA;EAEb,KAAA,EAAO,KAAK;EAAL;;;;AAMyB;EAAhC,YAAA,IAAgB,QAAA;AAAA;;;AAgBoC;;;;;;;;;;;iBAAtC,gBAAA,CAAiB,KAAqB,EAAd,cAAc"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{onCLS,onFCP,onINP,onLCP,onTTFB}from"web-vitals";const LOAD_VITALS=new Set([`TTFB`,`FCP`,`LCP`]);function captureWebVitals(input){if(typeof window>`u`)return;let{meter,resolveRoute,flush}=input,lcp=meter.createHistogram(`web_vitals.lcp`,{description:`Largest Contentful Paint`,unit:`s`,advice:{explicitBucketBoundaries:[.5,1,1.5,2,2.5,3,4,5,7.5,10]}}),fcp=meter.createHistogram(`web_vitals.fcp`,{description:`First Contentful Paint`,unit:`s`,advice:{explicitBucketBoundaries:[.5,1,1.5,1.8,2,2.5,3,4,5,7.5]}}),ttfb=meter.createHistogram(`web_vitals.ttfb`,{description:`Time to First Byte`,unit:`s`,advice:{explicitBucketBoundaries:[.1,.2,.3,.5,.8,1,1.5,2,3,5]}}),inp=meter.createHistogram(`web_vitals.inp`,{description:`Interaction to Next Paint`,unit:`s`,advice:{explicitBucketBoundaries:[.05,.1,.15,.2,.3,.4,.5,.75,1,2]}}),cls=meter.createHistogram(`web_vitals.cls`,{description:`Cumulative Layout Shift`,unit:`1`,advice:{explicitBucketBoundaries:[.01,.025,.05,.075,.1,.15,.2,.25,.5,1]}}),vitals={LCP:{histogram:lcp,toSeconds:!0},FCP:{histogram:fcp,toSeconds:!0},TTFB:{histogram:ttfb,toSeconds:!0},INP:{histogram:inp,toSeconds:!0},CLS:{histogram:cls,toSeconds:!1}},received=new Set,flushed=!1;function report(metric){let entry=vitals[metric.name];if(!entry)return;let value=entry.toSeconds?metric.value*.001:metric.value,pathname=window.location.pathname,route=resolveRoute?.(pathname)??pathname;entry.histogram.record(value,{...metric.rating?{"web_vital.rating":metric.rating}:{},...metric.navigationType?{"web_vital.navigation_type":metric.navigationType}:{},"url.path":route}),!flushed&&LOAD_VITALS.has(metric.name)&&(received.add(metric.name),received.size===LOAD_VITALS.size&&(flushed=!0,flush().catch(()=>void 0)))}onLCP(report),onCLS(report),onINP(report),onTTFB(report),onFCP(report)}export{captureWebVitals};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-vitals.mjs","names":[],"sources":["../../../src/internal/otel/web-vitals.ts"],"sourcesContent":["import type { Histogram, Meter } from \"@opentelemetry/api\";\nimport { onCLS, onFCP, onINP, onLCP, onTTFB } from \"web-vitals\";\n\nconst ATTR_URL_PATH = \"url.path\" as const;\nconst MS_TO_S = 1 / 1000;\n\n/**\n * Vitals that fire during the page-load sequence — once we've received\n * all three we issue a one-shot flush so short visits still export\n * data. CLS and INP fire on `visibilitychange` which the kernel\n * already auto-flushes on, so they don't need their own trigger.\n */\nconst LOAD_VITALS = new Set([\"TTFB\", \"FCP\", \"LCP\"]);\n\ninterface VitalEntry {\n histogram: Histogram;\n toSeconds: boolean;\n}\n\nexport interface WebVitalsInput {\n /**\n * Called after the page-load vitals have all fired. The kernel passes\n * its own `flush()` so short-visit exports don't depend on the\n * `visibilitychange` listener firing.\n */\n flush: () => Promise<void>;\n /** OTel meter from the kernel's private provider. */\n meter: Meter;\n /**\n * Pathname → low-cardinality route template (e.g. `/blog/[slug]`).\n * Without this, dynamic routes produce one unique `url.path` label\n * per visited slug — a metric cardinality hazard.\n */\n resolveRoute?: (pathname: string) => string | undefined;\n}\n\n/**\n * Subscribes to Core Web Vitals (LCP/FCP/TTFB/INP/CLS) and records OTel\n * histograms. Time-based vitals are converted to seconds per\n * observability convention; CLS is dimensionless and recorded as-is.\n *\n * Each record carries `web_vital.rating`, `web_vital.navigation_type`,\n * and `url.path` (templated when `resolveRoute` is provided). Browser\n * and device dimensions come from the resource attached to the\n * MeterProvider, so every histogram datapoint already carries them.\n *\n * Bucket boundaries match the rum.ts SDK — same dashboards, same\n * histograms, same percentile shape.\n */\nexport function captureWebVitals(input: WebVitalsInput): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const { meter, resolveRoute, flush } = input;\n\n const lcp = meter.createHistogram(\"web_vitals.lcp\", {\n description: \"Largest Contentful Paint\",\n unit: \"s\",\n advice: {\n explicitBucketBoundaries: [0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 7.5, 10],\n },\n });\n const fcp = meter.createHistogram(\"web_vitals.fcp\", {\n description: \"First Contentful Paint\",\n unit: \"s\",\n advice: {\n explicitBucketBoundaries: [0.5, 1, 1.5, 1.8, 2, 2.5, 3, 4, 5, 7.5],\n },\n });\n const ttfb = meter.createHistogram(\"web_vitals.ttfb\", {\n description: \"Time to First Byte\",\n unit: \"s\",\n advice: {\n explicitBucketBoundaries: [0.1, 0.2, 0.3, 0.5, 0.8, 1, 1.5, 2, 3, 5],\n },\n });\n const inp = meter.createHistogram(\"web_vitals.inp\", {\n description: \"Interaction to Next Paint\",\n unit: \"s\",\n advice: {\n explicitBucketBoundaries: [\n 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2,\n ],\n },\n });\n const cls = meter.createHistogram(\"web_vitals.cls\", {\n description: \"Cumulative Layout Shift\",\n unit: \"1\",\n advice: {\n explicitBucketBoundaries: [\n 0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.25, 0.5, 1,\n ],\n },\n });\n\n const vitals: Record<string, VitalEntry> = {\n LCP: { histogram: lcp, toSeconds: true },\n FCP: { histogram: fcp, toSeconds: true },\n TTFB: { histogram: ttfb, toSeconds: true },\n INP: { histogram: inp, toSeconds: true },\n CLS: { histogram: cls, toSeconds: false },\n };\n\n const received = new Set<string>();\n let flushed = false;\n\n function report(metric: {\n name: string;\n value: number;\n rating?: string;\n navigationType?: string;\n }): void {\n const entry = vitals[metric.name];\n if (!entry) {\n return;\n }\n const value = entry.toSeconds ? metric.value * MS_TO_S : metric.value;\n const pathname = window.location.pathname;\n const route = resolveRoute?.(pathname) ?? pathname;\n\n entry.histogram.record(value, {\n ...(metric.rating ? { \"web_vital.rating\": metric.rating } : {}),\n ...(metric.navigationType\n ? { \"web_vital.navigation_type\": metric.navigationType }\n : {}),\n [ATTR_URL_PATH]: route,\n });\n\n if (!flushed && LOAD_VITALS.has(metric.name)) {\n received.add(metric.name);\n if (received.size === LOAD_VITALS.size) {\n flushed = true;\n flush().catch(() => undefined);\n }\n }\n }\n\n onLCP(report);\n onCLS(report);\n onINP(report);\n onTTFB(report);\n onFCP(report);\n}\n"],"mappings":"uDAGA,MASM,YAAc,IAAI,IAAI,CAAC,OAAQ,MAAO,KAAK,CAAC,EAqClD,SAAgB,iBAAiB,MAA6B,CAC5D,GAAI,OAAO,OAAW,IACpB,OAGF,GAAM,CAAE,MAAO,aAAc,OAAU,MAEjC,IAAM,MAAM,gBAAgB,iBAAkB,CAClD,YAAa,2BACb,KAAM,IACN,OAAQ,CACN,yBAA0B,CAAC,GAAK,EAAG,IAAK,EAAG,IAAK,EAAG,EAAG,EAAG,IAAK,EAAE,CAClE,CACF,CAAC,EACK,IAAM,MAAM,gBAAgB,iBAAkB,CAClD,YAAa,yBACb,KAAM,IACN,OAAQ,CACN,yBAA0B,CAAC,GAAK,EAAG,IAAK,IAAK,EAAG,IAAK,EAAG,EAAG,EAAG,GAAG,CACnE,CACF,CAAC,EACK,KAAO,MAAM,gBAAgB,kBAAmB,CACpD,YAAa,qBACb,KAAM,IACN,OAAQ,CACN,yBAA0B,CAAC,GAAK,GAAK,GAAK,GAAK,GAAK,EAAG,IAAK,EAAG,EAAG,CAAC,CACrE,CACF,CAAC,EACK,IAAM,MAAM,gBAAgB,iBAAkB,CAClD,YAAa,4BACb,KAAM,IACN,OAAQ,CACN,yBAA0B,CACxB,IAAM,GAAK,IAAM,GAAK,GAAK,GAAK,GAAK,IAAM,EAAG,CAChD,CACF,CACF,CAAC,EACK,IAAM,MAAM,gBAAgB,iBAAkB,CAClD,YAAa,0BACb,KAAM,IACN,OAAQ,CACN,yBAA0B,CACxB,IAAM,KAAO,IAAM,KAAO,GAAK,IAAM,GAAK,IAAM,GAAK,CACvD,CACF,CACF,CAAC,EAEK,OAAqC,CACzC,IAAK,CAAE,UAAW,IAAK,UAAW,EAAK,EACvC,IAAK,CAAE,UAAW,IAAK,UAAW,EAAK,EACvC,KAAM,CAAE,UAAW,KAAM,UAAW,EAAK,EACzC,IAAK,CAAE,UAAW,IAAK,UAAW,EAAK,EACvC,IAAK,CAAE,UAAW,IAAK,UAAW,EAAM,CAC1C,EAEM,SAAW,IAAI,IACjB,QAAU,GAEd,SAAS,OAAO,OAKP,CACP,IAAM,MAAQ,OAAO,OAAO,MAC5B,GAAI,CAAC,MACH,OAEF,IAAM,MAAQ,MAAM,UAAY,OAAO,MAAQ,KAAU,OAAO,MAC1D,SAAW,OAAO,SAAS,SAC3B,MAAQ,eAAe,QAAQ,GAAK,SAE1C,MAAM,UAAU,OAAO,MAAO,CAC5B,GAAI,OAAO,OAAS,CAAE,mBAAoB,OAAO,MAAO,EAAI,CAAC,EAC7D,GAAI,OAAO,eACP,CAAE,4BAA6B,OAAO,cAAe,EACrD,CAAC,EACJ,WAAgB,KACnB,CAAC,EAEG,CAAC,SAAW,YAAY,IAAI,OAAO,IAAI,IACzC,SAAS,IAAI,OAAO,IAAI,EACpB,SAAS,OAAS,YAAY,OAChC,QAAU,GACV,MAAM,EAAE,UAAY,IAAA,EAAS,GAGnC,CAEA,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,OAAO,MAAM,EACb,MAAM,MAAM,CACd"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/internal/page-lifecycle.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Single subscription point for `visibilitychange` (hidden) and
|
|
4
|
+
* `beforeunload`. Modules call `onPageHidden(handler)` and the handler
|
|
5
|
+
* fires once per transition into hidden / unload.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the previous shape where the kernel, the queue, the pages
|
|
8
|
+
* plugin, and the replay plugin each attached their own listeners — four
|
|
9
|
+
* subscribers racing to flush / drain / emit on the same boundary, with
|
|
10
|
+
* no shared lifecycle. One subscription, many handlers, no coordination
|
|
11
|
+
* via the global event subsystem.
|
|
12
|
+
*
|
|
13
|
+
* Both events can fire (visibilitychange first when the user navigates
|
|
14
|
+
* away, then sometimes beforeunload). Handlers must be idempotent.
|
|
15
|
+
*/
|
|
16
|
+
type Handler = () => void;
|
|
17
|
+
declare function onPageHidden(handler: Handler): () => void;
|
|
18
|
+
/** Test hook — drops every handler and unbinds the listeners. */
|
|
19
|
+
declare function _resetPageLifecycleForTests(): void;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { _resetPageLifecycleForTests, onPageHidden };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-lifecycle.d.mts","names":[],"sources":["../../src/internal/page-lifecycle.ts"],"mappings":";;;;;;AAcY;AA6BZ;;;;AAA6C;AAS7C;;;KAtCK,OAAA;AAAA,iBA6BW,YAAA,CAAa,OAAgB,EAAP,OAAO;;iBAS7B,2BAAA,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const handlers=new Set;let installed=!1;function fire(){for(let handler of handlers)handler()}function onVisibilityChange(){typeof document<`u`&&document.visibilityState===`hidden`&&fire()}function install(){installed||typeof globalThis.addEventListener!=`function`||(installed=!0,globalThis.addEventListener(`visibilitychange`,onVisibilityChange),globalThis.addEventListener(`beforeunload`,fire))}function onPageHidden(handler){return install(),handlers.add(handler),()=>{handlers.delete(handler)}}function _resetPageLifecycleForTests(){handlers.clear(),installed&&typeof globalThis.removeEventListener==`function`&&(globalThis.removeEventListener(`visibilitychange`,onVisibilityChange),globalThis.removeEventListener(`beforeunload`,fire)),installed=!1}export{_resetPageLifecycleForTests,onPageHidden};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-lifecycle.mjs","names":[],"sources":["../../src/internal/page-lifecycle.ts"],"sourcesContent":["/**\n * Single subscription point for `visibilitychange` (hidden) and\n * `beforeunload`. Modules call `onPageHidden(handler)` and the handler\n * fires once per transition into hidden / unload.\n *\n * Replaces the previous shape where the kernel, the queue, the pages\n * plugin, and the replay plugin each attached their own listeners — four\n * subscribers racing to flush / drain / emit on the same boundary, with\n * no shared lifecycle. One subscription, many handlers, no coordination\n * via the global event subsystem.\n *\n * Both events can fire (visibilitychange first when the user navigates\n * away, then sometimes beforeunload). Handlers must be idempotent.\n */\ntype Handler = () => void;\n\nconst handlers = new Set<Handler>();\nlet installed = false;\n\nfunction fire(): void {\n for (const handler of handlers) {\n handler();\n }\n}\n\nfunction onVisibilityChange(): void {\n if (\n typeof document !== \"undefined\" &&\n document.visibilityState === \"hidden\"\n ) {\n fire();\n }\n}\n\nfunction install(): void {\n if (installed || typeof globalThis.addEventListener !== \"function\") {\n return;\n }\n installed = true;\n globalThis.addEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", fire);\n}\n\nexport function onPageHidden(handler: Handler): () => void {\n install();\n handlers.add(handler);\n return () => {\n handlers.delete(handler);\n };\n}\n\n/** Test hook — drops every handler and unbinds the listeners. */\nexport function _resetPageLifecycleForTests(): void {\n handlers.clear();\n if (installed && typeof globalThis.removeEventListener === \"function\") {\n globalThis.removeEventListener(\"visibilitychange\", onVisibilityChange);\n globalThis.removeEventListener(\"beforeunload\", fire);\n }\n installed = false;\n}\n"],"mappings":"AAgBA,MAAM,SAAW,IAAI,IACrB,IAAI,UAAY,GAEhB,SAAS,MAAa,CACpB,IAAK,IAAM,WAAW,SACpB,QAAQ,CAEZ,CAEA,SAAS,oBAA2B,CAEhC,OAAO,SAAa,KACpB,SAAS,kBAAoB,UAE7B,KAAK,CAET,CAEA,SAAS,SAAgB,CACnB,WAAa,OAAO,WAAW,kBAAqB,aAGxD,UAAY,GACZ,WAAW,iBAAiB,mBAAoB,kBAAkB,EAClE,WAAW,iBAAiB,eAAgB,IAAI,EAClD,CAEA,SAAgB,aAAa,QAA8B,CAGzD,OAFA,QAAQ,EACR,SAAS,IAAI,OAAO,MACP,CACX,SAAS,OAAO,OAAO,CACzB,CACF,CAGA,SAAgB,6BAAoC,CAClD,SAAS,MAAM,EACX,WAAa,OAAO,WAAW,qBAAwB,aACzD,WAAW,oBAAoB,mBAAoB,kBAAkB,EACrE,WAAW,oBAAoB,eAAgB,IAAI,GAErD,UAAY,EACd"}
|
|
@@ -2,7 +2,6 @@ import { PluginContext } from "../plugins/lib/types.mjs";
|
|
|
2
2
|
import { PluginOverrides } from "../plugins/lib/loader.mjs";
|
|
3
3
|
import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
|
|
4
4
|
import { RemotePluginConfig } from "@interfere/types/sdk/remote-config";
|
|
5
|
-
import { EventType } from "@interfere/types/sdk/envelope";
|
|
6
5
|
|
|
7
6
|
//#region src/internal/plugin-runtime.d.ts
|
|
8
7
|
declare class PluginRuntime {
|
|
@@ -19,7 +18,6 @@ declare class PluginRuntime {
|
|
|
19
18
|
setConsent(nextConsent?: ConsentState): void;
|
|
20
19
|
resetConsent(): void;
|
|
21
20
|
applyRemoteConfig(config: RemotePluginConfig): void;
|
|
22
|
-
canCapture(type: EventType): boolean;
|
|
23
21
|
start(): void;
|
|
24
22
|
dispose(): Promise<void>;
|
|
25
23
|
private shouldEnablePlugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin-runtime.d.mts","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"mappings":";;;;;;cAwBa,aAAA;EAAA,iBACM,cAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,YAAA;EAAA,QACA,YAAA;EAAA,QACA,WAAA;EAAA,QACA,QAAA;cAGN,OAAA,EAAS,aAAA,EACT,SAAA,EAAW,eAAA,cACX,cAAA,EAAgB,YAAA;EAOlB,UAAA,CAAA,GAAc,YAAA;EAId,UAAA,CAAW,WAAA,GAAc,YAAA;EAUzB,YAAA,CAAA;EASA,iBAAA,CAAkB,MAAA,EAAQ,kBAAA;EAK1B,KAAA,CAAA;EAWM,OAAA,CAAA,GAAW,OAAA;EAAA,QAWT,kBAAA;EAAA,QAQA,UAAA;EAAA,QAeM,QAAA;EAAA,QAoBN,IAAA;AAAA"}
|
|
@@ -1,107 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import errorsPlugin from "../plugins/errors.mjs";
|
|
3
|
-
import { loadPlugin, resolveFeatures } from "../plugins/lib/loader.mjs";
|
|
4
|
-
import { getPluginConsentCategory, hasConsentChanged, isConsentAllowed, resolveGrantedConsent, shouldCaptureEvent } from "./consent.mjs";
|
|
5
|
-
import { PLUGIN_MANIFEST } from "@interfere/types/sdk/plugins/manifest";
|
|
6
|
-
import { EVENT_TYPE_TO_PLUGIN } from "@interfere/types/sdk/remote-config";
|
|
7
|
-
//#region src/internal/plugin-runtime.ts
|
|
8
|
-
const log = createLogger("plugin-runtime");
|
|
9
|
-
var PluginRuntime = class {
|
|
10
|
-
activeCleanups = /* @__PURE__ */ new Map();
|
|
11
|
-
pending = /* @__PURE__ */ new Set();
|
|
12
|
-
context;
|
|
13
|
-
features;
|
|
14
|
-
remoteConfig = {};
|
|
15
|
-
consentState;
|
|
16
|
-
syncVersion = 0;
|
|
17
|
-
disposed = false;
|
|
18
|
-
constructor(context, overrides, initialConsent) {
|
|
19
|
-
this.context = context;
|
|
20
|
-
this.features = resolveFeatures(overrides);
|
|
21
|
-
this.consentState = initialConsent ?? null;
|
|
22
|
-
}
|
|
23
|
-
getConsent() {
|
|
24
|
-
return this.consentState;
|
|
25
|
-
}
|
|
26
|
-
setConsent(nextConsent) {
|
|
27
|
-
const nextState = resolveGrantedConsent(nextConsent);
|
|
28
|
-
if (!hasConsentChanged(this.consentState, nextState)) return;
|
|
29
|
-
this.consentState = nextState;
|
|
30
|
-
this.sync();
|
|
31
|
-
}
|
|
32
|
-
resetConsent() {
|
|
33
|
-
if (!hasConsentChanged(this.consentState, null)) return;
|
|
34
|
-
this.consentState = null;
|
|
35
|
-
this.sync();
|
|
36
|
-
}
|
|
37
|
-
applyRemoteConfig(config) {
|
|
38
|
-
this.remoteConfig = config;
|
|
39
|
-
this.sync();
|
|
40
|
-
}
|
|
41
|
-
canCapture(type) {
|
|
42
|
-
const plugin = EVENT_TYPE_TO_PLUGIN[type];
|
|
43
|
-
if (plugin && this.remoteConfig[plugin] === false) return false;
|
|
44
|
-
return shouldCaptureEvent(type, this.consentState);
|
|
45
|
-
}
|
|
46
|
-
start() {
|
|
47
|
-
if (this.shouldEnablePlugin("errors")) {
|
|
48
|
-
const cleanup = errorsPlugin.setup(this.context);
|
|
49
|
-
if (cleanup) this.activeCleanups.set("errors", cleanup);
|
|
50
|
-
}
|
|
51
|
-
this.sync();
|
|
52
|
-
}
|
|
53
|
-
async dispose() {
|
|
54
|
-
this.disposed = true;
|
|
55
|
-
this.syncVersion += 1;
|
|
56
|
-
for (const key of this.activeCleanups.keys()) this.deactivate(key);
|
|
57
|
-
await Promise.allSettled(this.pending);
|
|
58
|
-
}
|
|
59
|
-
shouldEnablePlugin(key) {
|
|
60
|
-
return this.features[key] && this.remoteConfig[key] !== false && isConsentAllowed(getPluginConsentCategory(key), this.consentState);
|
|
61
|
-
}
|
|
62
|
-
deactivate(key) {
|
|
63
|
-
const cleanup = this.activeCleanups.get(key);
|
|
64
|
-
if (!cleanup) return;
|
|
65
|
-
try {
|
|
66
|
-
cleanup();
|
|
67
|
-
} catch {
|
|
68
|
-
log.warn("cleanup failed for %s", key);
|
|
69
|
-
}
|
|
70
|
-
this.activeCleanups.delete(key);
|
|
71
|
-
}
|
|
72
|
-
async activate(key) {
|
|
73
|
-
if (this.activeCleanups.has(key) || !this.shouldEnablePlugin(key)) return;
|
|
74
|
-
const version = this.syncVersion;
|
|
75
|
-
const cleanup = await loadPlugin(key, this.context);
|
|
76
|
-
if (!cleanup) return;
|
|
77
|
-
if (version !== this.syncVersion || !this.shouldEnablePlugin(key)) {
|
|
78
|
-
cleanup();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
this.activeCleanups.set(key, cleanup);
|
|
82
|
-
}
|
|
83
|
-
sync() {
|
|
84
|
-
if (this.disposed) return;
|
|
85
|
-
this.syncVersion += 1;
|
|
86
|
-
for (const plugin of PLUGIN_MANIFEST) {
|
|
87
|
-
if (this.shouldEnablePlugin(plugin.name)) {
|
|
88
|
-
if (plugin.name === "errors") {
|
|
89
|
-
if (!this.activeCleanups.has("errors")) {
|
|
90
|
-
const cleanup = errorsPlugin.setup(this.context);
|
|
91
|
-
if (cleanup) this.activeCleanups.set("errors", cleanup);
|
|
92
|
-
}
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const p = this.activate(plugin.name).catch(() => {
|
|
96
|
-
log.warn("non-critical plugin loading failed");
|
|
97
|
-
});
|
|
98
|
-
this.pending.add(p);
|
|
99
|
-
p.finally(() => this.pending.delete(p));
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
this.deactivate(plugin.name);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
//#endregion
|
|
107
|
-
export { PluginRuntime };
|
|
1
|
+
import{createLogger}from"../util/log.mjs";import{getPluginConsentCategory,hasConsentChanged,isConsentAllowed,resolveGrantedConsent}from"./consent.mjs";import errorsPlugin from"../plugins/errors.mjs";import{loadPlugin,resolveFeatures}from"../plugins/lib/loader.mjs";import{PLUGIN_MANIFEST}from"@interfere/types/sdk/plugins/manifest";const log=createLogger(`plugin-runtime`);var PluginRuntime=class{activeCleanups=new Map;pending=new Set;context;features;remoteConfig={};consentState;syncVersion=0;disposed=!1;constructor(context,overrides,initialConsent){this.context=context,this.features=resolveFeatures(overrides),this.consentState=initialConsent??null}getConsent(){return this.consentState}setConsent(nextConsent){let nextState=resolveGrantedConsent(nextConsent);hasConsentChanged(this.consentState,nextState)&&(this.consentState=nextState,this.sync())}resetConsent(){hasConsentChanged(this.consentState,null)&&(this.consentState=null,this.sync())}applyRemoteConfig(config){this.remoteConfig=config,this.sync()}start(){if(this.shouldEnablePlugin(`errors`)){let cleanup=errorsPlugin.setup(this.context);cleanup&&this.activeCleanups.set(`errors`,cleanup)}this.sync()}async dispose(){this.disposed=!0,this.syncVersion+=1;for(let key of this.activeCleanups.keys())this.deactivate(key);await Promise.allSettled(this.pending)}shouldEnablePlugin(key){return this.features[key]&&this.remoteConfig[key]!==!1&&isConsentAllowed(getPluginConsentCategory(key),this.consentState)}deactivate(key){let cleanup=this.activeCleanups.get(key);if(cleanup){try{cleanup()}catch{log.warn(`cleanup failed for %s`,key)}this.activeCleanups.delete(key)}}async activate(key){if(this.activeCleanups.has(key)||!this.shouldEnablePlugin(key))return;let version=this.syncVersion,cleanup=await loadPlugin(key,this.context);if(cleanup){if(version!==this.syncVersion||!this.shouldEnablePlugin(key)){cleanup();return}this.activeCleanups.set(key,cleanup)}}sync(){if(!this.disposed){this.syncVersion+=1;for(let plugin of PLUGIN_MANIFEST){if(this.shouldEnablePlugin(plugin.name)){if(plugin.name===`errors`){if(!this.activeCleanups.has(`errors`)){let cleanup=errorsPlugin.setup(this.context);cleanup&&this.activeCleanups.set(`errors`,cleanup)}continue}let p=this.activate(plugin.name).catch(()=>{log.warn(`non-critical plugin loading failed`)});this.pending.add(p),p.finally(()=>this.pending.delete(p));continue}this.deactivate(plugin.name)}}}};export{PluginRuntime};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-runtime.mjs","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"plugin-runtime.mjs","names":[],"sources":["../../src/internal/plugin-runtime.ts"],"sourcesContent":["import {\n type ConsentState,\n PLUGIN_MANIFEST,\n type PluginKey,\n} from \"@interfere/types/sdk/plugins/manifest\";\nimport type { RemotePluginConfig } from \"@interfere/types/sdk/remote-config\";\n\nimport errorsPlugin from \"../plugins/errors.js\";\nimport {\n loadPlugin,\n type PluginOverrides,\n resolveFeatures,\n} from \"../plugins/lib/loader.js\";\nimport type { PluginCleanup, PluginContext } from \"../plugins/lib/types.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getPluginConsentCategory,\n hasConsentChanged,\n isConsentAllowed,\n resolveGrantedConsent,\n} from \"./consent.js\";\n\nconst log = createLogger(\"plugin-runtime\");\n\nexport class PluginRuntime {\n private readonly activeCleanups = new Map<PluginKey, PluginCleanup>();\n private readonly pending = new Set<Promise<void>>();\n private readonly context: PluginContext;\n private readonly features: Record<PluginKey, boolean>;\n private remoteConfig: RemotePluginConfig = {};\n private consentState: ConsentState | null;\n private syncVersion = 0;\n private disposed = false;\n\n constructor(\n context: PluginContext,\n overrides: PluginOverrides | undefined,\n initialConsent: ConsentState | undefined\n ) {\n this.context = context;\n this.features = resolveFeatures(overrides);\n this.consentState = initialConsent ?? null;\n }\n\n getConsent(): ConsentState | null {\n return this.consentState;\n }\n\n setConsent(nextConsent?: ConsentState): void {\n const nextState = resolveGrantedConsent(nextConsent);\n if (!hasConsentChanged(this.consentState, nextState)) {\n return;\n }\n\n this.consentState = nextState;\n this.sync();\n }\n\n resetConsent(): void {\n if (!hasConsentChanged(this.consentState, null)) {\n return;\n }\n\n this.consentState = null;\n this.sync();\n }\n\n applyRemoteConfig(config: RemotePluginConfig): void {\n this.remoteConfig = config;\n this.sync();\n }\n\n start(): void {\n if (this.shouldEnablePlugin(\"errors\")) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n\n this.sync();\n }\n\n async dispose(): Promise<void> {\n this.disposed = true;\n this.syncVersion += 1;\n\n for (const key of this.activeCleanups.keys()) {\n this.deactivate(key);\n }\n\n await Promise.allSettled(this.pending);\n }\n\n private shouldEnablePlugin(key: PluginKey): boolean {\n return (\n this.features[key] &&\n this.remoteConfig[key] !== false &&\n isConsentAllowed(getPluginConsentCategory(key), this.consentState)\n );\n }\n\n private deactivate(key: PluginKey): void {\n const cleanup = this.activeCleanups.get(key);\n if (!cleanup) {\n return;\n }\n\n try {\n cleanup();\n } catch {\n log.warn(\"cleanup failed for %s\", key);\n }\n\n this.activeCleanups.delete(key);\n }\n\n private async activate(key: PluginKey): Promise<void> {\n if (this.activeCleanups.has(key) || !this.shouldEnablePlugin(key)) {\n return;\n }\n\n const version = this.syncVersion;\n const cleanup = await loadPlugin(key, this.context);\n if (!cleanup) {\n return;\n }\n\n const staleSync = version !== this.syncVersion;\n if (staleSync || !this.shouldEnablePlugin(key)) {\n cleanup();\n return;\n }\n\n this.activeCleanups.set(key, cleanup);\n }\n\n private sync(): void {\n if (this.disposed) {\n return;\n }\n\n this.syncVersion += 1;\n\n for (const plugin of PLUGIN_MANIFEST) {\n if (this.shouldEnablePlugin(plugin.name)) {\n if (plugin.name === \"errors\") {\n if (!this.activeCleanups.has(\"errors\")) {\n const cleanup = errorsPlugin.setup(this.context);\n if (cleanup) {\n this.activeCleanups.set(\"errors\", cleanup);\n }\n }\n continue;\n }\n\n const p = this.activate(plugin.name).catch(() => {\n log.warn(\"non-critical plugin loading failed\");\n });\n this.pending.add(p);\n p.finally(() => this.pending.delete(p));\n continue;\n }\n\n this.deactivate(plugin.name);\n }\n }\n}\n"],"mappings":"4UAsBA,MAAM,IAAM,aAAa,gBAAgB,EAEzC,IAAa,cAAb,KAA2B,CACzB,eAAkC,IAAI,IACtC,QAA2B,IAAI,IAC/B,QACA,SACA,aAA2C,CAAC,EAC5C,aACA,YAAsB,EACtB,SAAmB,GAEnB,YACE,QACA,UACA,eACA,CACA,KAAK,QAAU,QACf,KAAK,SAAW,gBAAgB,SAAS,EACzC,KAAK,aAAe,gBAAkB,IACxC,CAEA,YAAkC,CAChC,OAAO,KAAK,YACd,CAEA,WAAW,YAAkC,CAC3C,IAAM,UAAY,sBAAsB,WAAW,EAC9C,kBAAkB,KAAK,aAAc,SAAS,IAInD,KAAK,aAAe,UACpB,KAAK,KAAK,EACZ,CAEA,cAAqB,CACd,kBAAkB,KAAK,aAAc,IAAI,IAI9C,KAAK,aAAe,KACpB,KAAK,KAAK,EACZ,CAEA,kBAAkB,OAAkC,CAClD,KAAK,aAAe,OACpB,KAAK,KAAK,CACZ,CAEA,OAAc,CACZ,GAAI,KAAK,mBAAmB,QAAQ,EAAG,CACrC,IAAM,QAAU,aAAa,MAAM,KAAK,OAAO,EAC3C,SACF,KAAK,eAAe,IAAI,SAAU,OAAO,CAE7C,CAEA,KAAK,KAAK,CACZ,CAEA,MAAM,SAAyB,CAC7B,KAAK,SAAW,GAChB,KAAK,aAAe,EAEpB,IAAK,IAAM,OAAO,KAAK,eAAe,KAAK,EACzC,KAAK,WAAW,GAAG,EAGrB,MAAM,QAAQ,WAAW,KAAK,OAAO,CACvC,CAEA,mBAA2B,IAAyB,CAClD,OACE,KAAK,SAAS,MACd,KAAK,aAAa,OAAS,IAC3B,iBAAiB,yBAAyB,GAAG,EAAG,KAAK,YAAY,CAErE,CAEA,WAAmB,IAAsB,CACvC,IAAM,QAAU,KAAK,eAAe,IAAI,GAAG,EACtC,WAIL,IAAI,CACF,QAAQ,CACV,MAAQ,CACN,IAAI,KAAK,wBAAyB,GAAG,CACvC,CAEA,KAAK,eAAe,OAAO,GAAG,CAF9B,CAGF,CAEA,MAAc,SAAS,IAA+B,CACpD,GAAI,KAAK,eAAe,IAAI,GAAG,GAAK,CAAC,KAAK,mBAAmB,GAAG,EAC9D,OAGF,IAAM,QAAU,KAAK,YACf,QAAU,MAAM,WAAW,IAAK,KAAK,OAAO,EAC7C,WAKL,IADkB,UAAY,KAAK,aAClB,CAAC,KAAK,mBAAmB,GAAG,EAAG,CAC9C,QAAQ,EACR,MACF,CAEA,KAAK,eAAe,IAAI,IAAK,OAAO,CAFpC,CAGF,CAEA,MAAqB,CACf,SAAK,SAIT,MAAK,aAAe,EAEpB,IAAK,IAAM,UAAU,gBAAiB,CACpC,GAAI,KAAK,mBAAmB,OAAO,IAAI,EAAG,CACxC,GAAI,OAAO,OAAS,SAAU,CAC5B,GAAI,CAAC,KAAK,eAAe,IAAI,QAAQ,EAAG,CACtC,IAAM,QAAU,aAAa,MAAM,KAAK,OAAO,EAC3C,SACF,KAAK,eAAe,IAAI,SAAU,OAAO,CAE7C,CACA,QACF,CAEA,IAAM,EAAI,KAAK,SAAS,OAAO,IAAI,EAAE,UAAY,CAC/C,IAAI,KAAK,oCAAoC,CAC/C,CAAC,EACD,KAAK,QAAQ,IAAI,CAAC,EAClB,EAAE,YAAc,KAAK,QAAQ,OAAO,CAAC,CAAC,EACtC,QACF,CAEA,KAAK,WAAW,OAAO,IAAI,CAC7B,CAvBoB,CAwBtB,CACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Kernel } from "./kernel.mjs";
|
|
2
|
+
import { ConsentState } from "@interfere/types/sdk/plugins/manifest";
|
|
3
|
+
import { IdentifyParams } from "@interfere/types/sdk/identify";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/react-context.d.ts
|
|
6
|
+
interface InterfereContextValue {
|
|
7
|
+
consent: {
|
|
8
|
+
get(): ConsentState | null;
|
|
9
|
+
set(state?: ConsentState): void;
|
|
10
|
+
};
|
|
11
|
+
device: {
|
|
12
|
+
getDeviceId(): string | null;
|
|
13
|
+
getFpHash(): string | null;
|
|
14
|
+
};
|
|
15
|
+
identity: {
|
|
16
|
+
get(): IdentifyParams | null;
|
|
17
|
+
set(params: IdentifyParams): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The active kernel, or `null` when the SDK isn't initialized yet — for
|
|
21
|
+
* example during server-side prerender, when `init()` is client-only.
|
|
22
|
+
* Accessor objects (consent/device/identity/session) above stay safe to
|
|
23
|
+
* call in either state; they no-op when the kernel is null.
|
|
24
|
+
*/
|
|
25
|
+
kernel: Kernel | null;
|
|
26
|
+
session: {
|
|
27
|
+
getId(): string | null;
|
|
28
|
+
getWindowId(): string | null;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Context value used when the provider is mounted without a kernel — every
|
|
33
|
+
* accessor returns `null` and mutations are no-ops. Lets `useInterfere()`
|
|
34
|
+
* work seamlessly across SSR/CSR boundaries (server gets nulls, client
|
|
35
|
+
* gets real values once the kernel resolves).
|
|
36
|
+
*/
|
|
37
|
+
declare const NULL_CONTEXT_VALUE: InterfereContextValue;
|
|
38
|
+
/**
|
|
39
|
+
* Lives in its own module so `provider.tsx` and `internal/capture-boundary.tsx`
|
|
40
|
+
* can both import it without forming an import cycle.
|
|
41
|
+
*/
|
|
42
|
+
declare const InterfereContext: import("react").Context<InterfereContextValue | null>;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { InterfereContext, InterfereContextValue, NULL_CONTEXT_VALUE };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-context.d.mts","names":[],"sources":["../../src/internal/react-context.ts"],"mappings":";;;;;UASiB,qBAAA;EACf,OAAA;IACE,GAAA,IAAO,YAAA;IACP,GAAA,CAAI,KAAA,GAAQ,YAAA;EAAA;EAEd,MAAA;IACE,WAAA;IACA,SAAA;EAAA;EAEF,QAAA;IACE,GAAA,IAAO,cAAA;IACP,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAAA;EAV/B;;;;;;EAkBA,MAAA,EAAQ,MAAA;EACR,OAAA;IACE,KAAA;IACA,WAAA;EAAA;AAAA;;;;;;;cAkBS,kBAAA,EAAoB,qBAMhC;;;;AAxBc;cA8BF,gBAAA,kBAAgB,OAAA,CAAA,qBAAA"}
|