@shopkit/observability 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,95 @@
1
- # Temporary Holding Version
1
+ # @shopkit/observability
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Frontend monitoring for storefronts and the editor. Browser errors, failed requests, broken images and web vitals are sent as OTLP traces to a relay on the app's own server, which forwards them to SigNoz.
4
+
5
+ The package knows no framework: each app passes its own paths and values.
6
+
7
+ ## Browser: `@shopkit/observability/client`
8
+
9
+ ```ts
10
+ import { initObservability } from "@shopkit/observability/client";
11
+
12
+ initObservability({
13
+ app: "storefront",
14
+ serviceName: "storefront-web",
15
+ relayUrl: "/api/obs",
16
+ ignore: { params: ["previewKey"], errors: [/ResizeObserver loop/] },
17
+ });
18
+ ```
19
+
20
+ About 4.6 KB gzip up front; `web-vitals` loads after page load. Never throws into the page. Each page view is its own trace; same-origin API calls join it. Next.js link prefetches are marked unsampled, so the server doesn't trace them.
21
+
22
+ Every signal carries `page.type` (from the optional `pageType(path)` callback) and, where the browser knows it, `network.effective_type`. Other options: `ignore.paths` / `ignore.cookies` (non-shopper traffic), `slowFetchMs` (default 2000), `sessionCap` (default 100 per page).
23
+
24
+ Business flows (checkout, add to cart):
25
+
26
+ ```ts
27
+ import { startFlow } from "@shopkit/observability/client";
28
+
29
+ const flow = startFlow("checkout"); // later: flow.success() or flow.fail("checkout_api", err)
30
+ ```
31
+
32
+ Each flow is recorded once as `flow.<name>` with `flow.outcome` and `flow.failure_reason`.
33
+
34
+ ## Before the app starts: `@shopkit/observability/head-stub`
35
+
36
+ ```tsx
37
+ import { headStubScript } from "@shopkit/observability/head-stub";
38
+
39
+ // First child of <head>, server-rendered
40
+ <script dangerouslySetInnerHTML={{ __html: headStubScript({ relayUrl, serviceName, app }) }} />
41
+ ```
42
+
43
+ An inline ES5 script (~1.2 KB gzip). It queues errors that happen before the app's JS runs, and `initObservability` sends them on. If the app never starts (its scripts failed), the stub itself reports `browser.app_not_started` with what it caught, once the page has loaded.
44
+
45
+ ## Containing crashes: `@shopkit/observability/react`
46
+
47
+ ```tsx
48
+ import { ObservabilityBoundary } from "@shopkit/observability/react";
49
+
50
+ <ObservabilityBoundary kind="widget" id={widget.id} type={widget.type}>
51
+ {widget}
52
+ </ObservabilityBoundary>
53
+ ```
54
+
55
+ A crash renders nothing (or `fallback`); the rest of the page stays up, and the crash is reported as `browser.render_error` with the component's kind, id and type. Plain React, already marked `"use client"`. Reports go through `initObservability`; before it runs, crashes are still contained.
56
+
57
+ For Next.js `error.tsx` / `global-error.tsx`: `useReportError(error, "app-error")` (the id names which boundary caught it).
58
+
59
+ ## Relay: `@shopkit/observability/relay` (Node)
60
+
61
+ ```ts
62
+ import { createRelayHandler, fileSourceMaps } from "@shopkit/observability/relay";
63
+
64
+ export const POST = createRelayHandler({
65
+ endpoint: "http://collector:4318/v1/traces",
66
+ // Set by the server; overwrites anything the browser sent.
67
+ // Release and environment belong here too: the head stub's reports carry neither.
68
+ trusted: { "merchant.id": merchantId, "service.version": release, "deployment.environment": env },
69
+ sourceMaps: fileSourceMaps({ urlPath: "/_next/static/", dir: ".next/obs-sourcemaps" }),
70
+ });
71
+ ```
72
+
73
+ Checks size (64 KB) and shape, rate-limits per server instance (client IPs can be faked), drops bots, strips query strings, URL fragments, emails and phone numbers, stamps `trusted` plus `device.type` and `browser.in_app` from the User-Agent, resolves minified stacks (at most 30 new maps a minute), forwards to the collector.
74
+
75
+ ## Server tracing: `@shopkit/observability/server` (Node)
76
+
77
+ Generic OpenTelemetry pieces for the app's own SDK setup (e.g. `@vercel/otel`'s `registerOTel`). Needs `@opentelemetry/api`.
78
+
79
+ ```ts
80
+ registerOTel({
81
+ serviceName: "storefront-server",
82
+ traceSampler: skipPathsSampler(/^\/(api\/obs$|_next\/)/), // never trace these paths
83
+ spanProcessors: [scrubSpanProcessor(), "auto"], // strip query strings, emails, phones
84
+ });
85
+ ```
86
+
87
+ ## Keeping source maps private: `shopkit-move-sourcemaps`
88
+
89
+ ```sh
90
+ shopkit-move-sourcemaps <public build dir> <private dir>
91
+ # e.g. after next build:
92
+ shopkit-move-sourcemaps .next/static .next/obs-sourcemaps
93
+ ```
94
+
95
+ Moves every `*.map` out of the served folder. Fails (exit 1) if it can't, so maps are never shipped publicly.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';var promises=require('fs/promises'),path=require('path');async function m(e,i){let u=await promises.readdir(e,{recursive:true});await promises.mkdir(i,{recursive:true});let t=u.filter(r=>r.endsWith(".map"));for(let r of t){let a=path.join(i,r);await promises.mkdir(path.dirname(a),{recursive:true}),await promises.rename(path.join(e,r),a);}return t.length}var[o,s]=process.argv.slice(2);(!o||!s)&&(console.error("usage: shopkit-move-sourcemaps <public build dir> <private dir>"),process.exit(1));m(o,s).then(e=>{e===0?console.warn(`observability: no source maps found in ${o}`):console.log(`observability: moved ${e} source maps from ${o} to ${s}`);},e=>{console.error(`observability: could not move source maps: ${e.message}`),process.exit(1);});
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import {readdir,mkdir,rename}from'fs/promises';import {join,dirname}from'path';async function m(e,i){let u=await readdir(e,{recursive:true});await mkdir(i,{recursive:true});let t=u.filter(r=>r.endsWith(".map"));for(let r of t){let a=join(i,r);await mkdir(dirname(a),{recursive:true}),await rename(join(e,r),a);}return t.length}var[o,s]=process.argv.slice(2);(!o||!s)&&(console.error("usage: shopkit-move-sourcemaps <public build dir> <private dir>"),process.exit(1));m(o,s).then(e=>{e===0?console.warn(`observability: no source maps found in ${o}`):console.log(`observability: moved ${e} source maps from ${o} to ${s}`);},e=>{console.error(`observability: could not move source maps: ${e.message}`),process.exit(1);});
@@ -0,0 +1 @@
1
+ var t="__shopkitObsStub",s="__shopkitObsBooted";export{t as a,s as b};
@@ -0,0 +1 @@
1
+ var r={SERVICE_NAME:"service.name",SERVICE_VERSION:"service.version",DEPLOYMENT_ENVIRONMENT:"deployment.environment",APP:"app",MERCHANT_ID:"merchant.id"},e={JS_ERROR:"browser.js_error",UNHANDLED_REJECTION:"browser.unhandled_rejection",RESOURCE_ERROR:"browser.resource_error",RENDER_ERROR:"browser.render_error",HYDRATION_ERROR:"browser.hydration_error",CHUNK_ERROR:"browser.chunk_error",FETCH:"browser.fetch",WEB_VITAL:"web_vital",RAGE_CLICK:"ui.rage_click",TELEMETRY_DROPPED:"telemetry.dropped",FLOW_PREFIX:"flow."};export{r as a,e as b};
@@ -0,0 +1,2 @@
1
+ var t=/\?[^\s#)'"]*?(?=(?::\d+:\d+)?(?:[\s#)'"]|$))/g,r=/(?<![\w.+-])[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g,i=/(?<!\w)(?:\+?91[\s-]?)?[6-9]\d{9}(?!\w)/g;function l(s){return s.replace(/\S+/g,e=>{let n=e.indexOf("#");if(n<0||!e.slice(0,n).includes("://"))return e;let c=e.slice(n).search(/[)'"]/);return e.slice(0,n)+(c<0?"":e.slice(n+c))})}function g(s){return l(s.replace(t,"")).replace(r,"[email]").replace(i,"[phone]")}
2
+ export{g as a};
@@ -0,0 +1,50 @@
1
+ import { App } from '../index.mjs';
2
+
3
+ interface ObservabilityConfig {
4
+ app: App;
5
+ serviceName: string;
6
+ /** Same-origin route on storefronts ("/api/obs"); the editor's backend URL. */
7
+ relayUrl: string;
8
+ release?: string;
9
+ environment?: string;
10
+ ignore?: {
11
+ /** Pages where nothing is installed, e.g. editor preview routes. */
12
+ paths?: RegExp[];
13
+ /** URL params that mark non-shopper traffic, e.g. "editorPreview". */
14
+ params?: string[];
15
+ /** Cookies whose presence marks non-shopper traffic, e.g. a preview session. */
16
+ cookies?: string[];
17
+ /** Error messages that are noise, e.g. /ResizeObserver loop/. */
18
+ errors?: RegExp[];
19
+ };
20
+ /** Max spans per page load. */
21
+ sessionCap?: number;
22
+ /** Successful same-origin API calls at least this slow are recorded. Default 2000. */
23
+ slowFetchMs?: number;
24
+ /** Names the kind of page from its path ("product", "collection"), for filtering. */
25
+ pageType?: (path: string) => string | undefined;
26
+ }
27
+ /**
28
+ * Starts browser capture. No-op on the server, on ignored pages, and when already started.
29
+ * Returns a function that stops capture. Never throws: e.g. `document.cookie` throws in
30
+ * sandboxed frames.
31
+ */
32
+ declare function initObservability(config: ObservabilityConfig): () => void;
33
+
34
+ type AttrValue = string | number | boolean;
35
+ type Attributes = Record<string, AttrValue | undefined>;
36
+
37
+ interface Flow {
38
+ success(attrs?: Attributes): void;
39
+ /** `reason` is a short fixed code (e.g. "checkout_api") to group failures by. */
40
+ fail(reason: string, err?: unknown, attrs?: Attributes): void;
41
+ }
42
+ /**
43
+ * Times one attempt of a business flow (add to cart, checkout) and records it once as
44
+ * a `flow.<name>` span with its outcome, so dashboards get attempts and failure rate.
45
+ * No exception event: an error the app also reports must not dedupe the flow away.
46
+ * Safe before init and on ignored pages (nothing is recorded); never throws.
47
+ */
48
+ declare function startFlow(name: string, attrs?: Attributes): Flow;
49
+
50
+ export { type Flow, type ObservabilityConfig, initObservability, startFlow };
@@ -0,0 +1,50 @@
1
+ import { App } from '../index.js';
2
+
3
+ interface ObservabilityConfig {
4
+ app: App;
5
+ serviceName: string;
6
+ /** Same-origin route on storefronts ("/api/obs"); the editor's backend URL. */
7
+ relayUrl: string;
8
+ release?: string;
9
+ environment?: string;
10
+ ignore?: {
11
+ /** Pages where nothing is installed, e.g. editor preview routes. */
12
+ paths?: RegExp[];
13
+ /** URL params that mark non-shopper traffic, e.g. "editorPreview". */
14
+ params?: string[];
15
+ /** Cookies whose presence marks non-shopper traffic, e.g. a preview session. */
16
+ cookies?: string[];
17
+ /** Error messages that are noise, e.g. /ResizeObserver loop/. */
18
+ errors?: RegExp[];
19
+ };
20
+ /** Max spans per page load. */
21
+ sessionCap?: number;
22
+ /** Successful same-origin API calls at least this slow are recorded. Default 2000. */
23
+ slowFetchMs?: number;
24
+ /** Names the kind of page from its path ("product", "collection"), for filtering. */
25
+ pageType?: (path: string) => string | undefined;
26
+ }
27
+ /**
28
+ * Starts browser capture. No-op on the server, on ignored pages, and when already started.
29
+ * Returns a function that stops capture. Never throws: e.g. `document.cookie` throws in
30
+ * sandboxed frames.
31
+ */
32
+ declare function initObservability(config: ObservabilityConfig): () => void;
33
+
34
+ type AttrValue = string | number | boolean;
35
+ type Attributes = Record<string, AttrValue | undefined>;
36
+
37
+ interface Flow {
38
+ success(attrs?: Attributes): void;
39
+ /** `reason` is a short fixed code (e.g. "checkout_api") to group failures by. */
40
+ fail(reason: string, err?: unknown, attrs?: Attributes): void;
41
+ }
42
+ /**
43
+ * Times one attempt of a business flow (add to cart, checkout) and records it once as
44
+ * a `flow.<name>` span with its outcome, so dashboards get attempts and failure rate.
45
+ * No exception event: an error the app also reports must not dedupe the flow away.
46
+ * Safe before init and on ignored pages (nothing is recorded); never throws.
47
+ */
48
+ declare function startFlow(name: string, attrs?: Attributes): Flow;
49
+
50
+ export { type Flow, type ObservabilityConfig, initObservability, startFlow };
@@ -0,0 +1 @@
1
+ 'use strict';var I={SERVICE_NAME:"service.name",SERVICE_VERSION:"service.version",DEPLOYMENT_ENVIRONMENT:"deployment.environment",APP:"app"},f={JS_ERROR:"browser.js_error",UNHANDLED_REJECTION:"browser.unhandled_rejection",RESOURCE_ERROR:"browser.resource_error",RENDER_ERROR:"browser.render_error",HYDRATION_ERROR:"browser.hydration_error",CHUNK_ERROR:"browser.chunk_error",FETCH:"browser.fetch",WEB_VITAL:"web_vital",RAGE_CLICK:"ui.rage_click",TELEMETRY_DROPPED:"telemetry.dropped",FLOW_PREFIX:"flow."};function R(){return performance.timeOrigin+performance.now()}function v(e){let t=crypto.getRandomValues(new Uint8Array(e)),r="";for(let o of t)r+=o.toString(16).padStart(2,"0");return /^0+$/.test(r)?v(e):r}var _=()=>v(16),y=()=>v(8);var L=1e3,j=8e3;function Y(e){return e instanceof Error?{"exception.type":e.name,"exception.message":e.message.slice(0,L),"exception.stacktrace":e.stack?.slice(0,j)}:{"exception.type":"Error","exception.message":String(e).slice(0,L)}}var X=new RegExp(["Loading (CSS )?chunk \\S+ failed","Failed to fetch dynamically imported module","Importing a module script failed","error loading dynamically imported module"].join("|"),"i");function J(e){return e instanceof Error?e.name==="ChunkLoadError"||X.test(e.message):false}var z=/Minified React error #(418|423|425)\b|Hydration failed|error while hydrating|did not match\. Server/i;function Q(e,t){return J(t)?f.CHUNK_ERROR:t instanceof Error&&z.test(t.message)?f.HYDRATION_ERROR:e}function A(e){function t(o,n,i={}){try{let s=R();e.emit({name:o,traceId:e.traceId,spanId:y(),parentSpanId:e.parentSpanId,startMs:s,endMs:s,...i,attributes:{"page.path":location.pathname,...n}});}catch{}}function r(o,n,i={},s={}){try{let c={name:"exception",timeMs:R(),attributes:Y(n)};t(Q(o,n),i,{...s,error:!0,events:[c]});}catch{}}return {record:t,recordException:r}}var C=e=>e.split(/[?#]/)[0];function Z(e){if(e instanceof HTMLImageElement||e instanceof HTMLScriptElement)return e.src;if(e instanceof HTMLLinkElement)return e.href}function U(e){let{record:t,recordException:r}=A(e),o=e.slowFetchMs??2e3;function n(d){if(d instanceof ErrorEvent){r(f.JS_ERROR,d.error??d.message);return}let p=Z(d.target);if(p===void 0)return;let u=d.target.tagName.toLowerCase(),a=u!=="img"&&new URL(p,location.href).origin!==location.origin;t(f.RESOURCE_ERROR,{"resource.tag":u,"resource.url":C(p),"resource.third_party":a||void 0},{error:!a});}function i(d){r(f.UNHANDLED_REJECTION,d.reason);}let s=new URL(e.relayUrl,location.href).href,c=window.fetch,m=c.bind(window);function g(d,p){try{let u=d instanceof Request?d:void 0,a=new URL(u?.url??String(d),location.href),l=C(a.href);return l===s?void 0:{method:(p?.method??u?.method??"GET").toUpperCase(),url:l,sameOrigin:a.origin===location.origin}}catch{return}}function h(d,p,u){try{let a=p?.headers??(d instanceof Request?d.headers:{}),l=new Headers(a);if(l.has("traceparent"))return p;let E=l.has("next-router-prefetch")?"00":"01";return l.set("traceparent",`00-${e.traceId}-${u}-${E}`),{...p,headers:l}}catch{return p}}async function S(d,p){let u=g(d,p);if(!u)return m(d,p);let a=`${f.FETCH} ${u.method}`,l={"http.method":u.method,"url.full":u.url},E=y(),w={kind:"client",startMs:R(),spanId:E};u.sameOrigin&&(p=h(d,p,E));try{let b=await m(d,p),x={...l,"http.status_code":b.status};return !b.ok&&b.type!=="opaque"?t(a,x,{...w,error:b.status>=500}):b.ok&&u.sameOrigin&&R()-w.startMs>=o&&t(a,x,{...w,error:!1}),b}catch(b){throw b instanceof DOMException&&b.name==="AbortError"||r(a,b,l,w),b}}return window.addEventListener("error",n,true),window.addEventListener("unhandledrejection",i),window.fetch=S,()=>{window.removeEventListener("error",n,true),window.removeEventListener("unhandledrejection",i),window.fetch===S&&(window.fetch=c);}}function ee(e){let t=e.events?.find(n=>n.name==="exception")?.attributes;if(!t)return;let{"exception.type":r,"exception.message":o}=t;return `${r}|${o}|${String(t["exception.stacktrace"]??"").slice(0,500)}`}function D(e=Date.now){let t=new Map;return function(o){let n=ee(o);if(n===void 0)return false;let i=e(),s=o.attributes?.["component.id"]!==void 0,c=t.get(n),m=c!==void 0&&i-c.at<1e3;return m&&(!s||c.boundary)?true:(t.size>=100&&t.clear(),t.set(n,{at:i,boundary:s||m&&c.boundary}),false)}}var T="__shopkitObsStub",V="__shopkitObsBooted";function P(){try{let e=window;e[V]=!0;let t=e[T];return delete e[T],t?.take()??[]}catch{return []}}function H(e,t){for(let r of t){let o={startMs:r.ts,endMs:r.ts};if(r.k==="r"){let n={"resource.tag":r.tag,"resource.url":r.url};e.record(f.RESOURCE_ERROR,n,{...o,error:true});}else {let n=new Error(r.message);n.name=r.type??"Error",n.stack=r.stack;let i=r.k==="u"?f.UNHANDLED_REJECTION:f.JS_ERROR;e.recordException(i,n,{},o);}}}var te=1e3,re=3,ne="input, textarea, select, [contenteditable]",oe="button, a, [role=button], summary, label";function ie(e){let t=[],r=e;for(let o=0;r&&r!==document.body&&o<3;o++){let n=r.tagName.toLowerCase();if(r.id){t.unshift(`${n}#${r.id}`);break}let i=Array.from(r.classList).slice(0,2);t.unshift(i.length?`${n}.${i.join(".")}`:n),r=r.parentElement;}return t.join(" > ").slice(0,200)}function F(e){let t=null,r=[],o=false;function n(i){try{let s=i.target instanceof Element?i.target:null;if(!s||s===document.body||s===document.documentElement||s.closest(ne))return;let c=s.closest(oe)??s,m=Date.now();c!==t&&(t=c,r=[]),r=r.filter(g=>m-g<te),r.push(m),r.length===1&&(o=!1),r.length>=re&&!o&&(o=!0,e(f.RAGE_CLICK,{"ui.target":ie(c)}));}catch{}}return document.addEventListener("click",n,true),()=>document.removeEventListener("click",n,true)}var B=Symbol.for("@shopkit/observability.reporter");function O(e){globalThis[B]=e;}function G(){return globalThis[B]}var se={internal:1,client:3},ae=0,ce=2;function N(e){let t=Math.floor(e),r=Math.round((e-t)*1e6);return r===1e6&&(t+=1,r=0),t===0?String(r):`${t}${String(r).padStart(6,"0")}`}function ue(e){if(typeof e=="string")return {stringValue:e};if(typeof e=="boolean")return {boolValue:e};if(Number.isFinite(e))return Number.isInteger(e)?{intValue:String(e)}:{doubleValue:e}}function M(e){let t=[];if(!e)return t;for(let r in e){let o=e[r],n=o===void 0?void 0:ue(o);n&&t.push({key:r,value:n});}return t}function de(e){return {traceId:e.traceId,spanId:e.spanId,...e.parentSpanId&&{parentSpanId:e.parentSpanId},name:e.name,kind:se[e.kind??"internal"],startTimeUnixNano:N(e.startMs),endTimeUnixNano:N(e.endMs),attributes:M(e.attributes),events:(e.events??[]).map(t=>({name:t.name,timeUnixNano:N(t.timeMs),attributes:M(t.attributes)})),status:{code:e.error?ce:ae}}}function K(e,t,r){return {resourceSpans:[{resource:{attributes:M(e)},scopeSpans:[{scope:t,spans:r.map(de)}]}]}}var pe=6e4,$="text/plain;charset=UTF-8";function q(e){let{url:t,resource:r,scope:o}=e,n=e.maxBatch??30,i=e.flushMs??5e3,s=e.sessionCap??100,c=[],m=0,g=0,h=0,S;function d(){if(g===h)return;let a=R(),l=g-h;return h=g,{name:f.TELEMETRY_DROPPED,traceId:e.traceId??_(),spanId:y(),startMs:a,endMs:a,attributes:{"telemetry.dropped_count":l,"page.path":location.pathname}}}function p(a,l){let E=JSON.stringify(K(r,o,a)),w=new Blob([E],{type:$});if(w.size>pe){if(a.length===1)return;let b=a.length>>1;p(a.slice(0,b),l),p(a.slice(b),l);return}l&&navigator.sendBeacon?.(t,w)||fetch(t,{method:"POST",body:E,headers:{"content-type":$},keepalive:l,credentials:"omit"}).catch(()=>{});}function u(a){clearTimeout(S),S=void 0;let l=a?d():void 0;if(l&&c.push(l),c.length===0)return;let E=c;c=[];try{p(E,a);}catch{}}return document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&u(true);}),window.addEventListener("pagehide",()=>u(true)),{add(a){if(m>=s){g++;return}m++,c.push(a),document.visibilityState==="hidden"?u(true):c.length>=n?u(false):S??(S=setTimeout(()=>u(false),i));}}}function W(e){let t=false;function r(n){if(t)return;let i=R();try{e.emit({name:f.WEB_VITAL,traceId:e.traceId,spanId:y(),parentSpanId:e.parentSpanId,startMs:i,endMs:i,attributes:{"page.path":location.pathname,"web_vital.name":n.name,"web_vital.value":n.value,"web_vital.rating":n.rating,"web_vital.navigation_type":n.navigationType}});}catch{}}function o(){import('web-vitals').then(({onCLS:n,onFCP:i,onINP:s,onLCP:c,onTTFB:m})=>{[n,i,s,c,m].forEach(g=>g(r));}).catch(()=>{});}return document.readyState==="complete"?o():window.addEventListener("load",o,{once:true}),()=>{t=true,window.removeEventListener("load",o);}}var k=Symbol.for("@shopkit/observability"),le=/^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;function fe(e){let t=e?le.exec(e.trim()):null;if(!(!t||/^0+$/.test(t[1])||/^0+$/.test(t[2])))return {traceId:t[1],parentSpanId:t[2]}}function me(){return document.cookie.split(";").map(e=>e.split("=")[0].trim())}function ge(e){if(e?.paths?.some(o=>o.test(location.pathname)))return true;let t=new URLSearchParams(location.search);if(e?.params?.some(o=>t.has(o)))return true;let r=e?.cookies?.length?me():[];return e?.cookies?.some(o=>r.includes(o))??false}function be(e){return String(e.events?.[0]?.attributes?.["exception.message"]??"")}function Re(e){try{return Ee(e)}catch{return ()=>{}}}function Ee(e){let t=globalThis;if(typeof window>"u")return ()=>{};let r=P();if(t[k]||ge(e.ignore))return ()=>{};t[k]=true;let o=document.querySelector('meta[name="traceparent"]')?.getAttribute("content"),n=fe(o)?.traceId,i={traceId:_()},s=q({url:e.relayUrl,resource:{[I.SERVICE_NAME]:e.serviceName,[I.APP]:e.app,[I.SERVICE_VERSION]:e.release,[I.DEPLOYMENT_ENVIRONMENT]:e.environment},scope:{name:"@shopkit/observability",version:"0.1.0"},sessionCap:e.sessionCap,traceId:i.traceId}),c=e.ignore?.errors??[],m=D(),g=u=>{let a=be(u);a&&c.some(l=>l.test(a))||m(u)||(u.attributes={...u.attributes,"page.type":e.pageType?.(location.pathname),"page.render_trace_id":n,"network.effective_type":navigator.connection?.effectiveType},s.add(u));},h=U({emit:g,...i,relayUrl:e.relayUrl,slowFetchMs:e.slowFetchMs}),S=W({emit:g,...i}),d=A({emit:g,...i});O(d),H(d,r);let p=F(d.record);return ()=>{h(),S(),p(),O(void 0),t[k]=false;}}var Se=1e3;function ye(e,t){let r=R(),o=false;function n(i,s){if(!o){o=true;try{G()?.record(f.FLOW_PREFIX+e,{...t,...s,"flow.outcome":i},{startMs:r,error:i==="failure"});}catch{}}}return {success:i=>n("success",{...i}),fail:(i,s,c)=>n("failure",{...c,"flow.failure_reason":i,"flow.error_message":s===void 0?void 0:(s instanceof Error?s.message:String(s)).slice(0,Se)})}}exports.initObservability=Re;exports.startFlow=ye;
@@ -0,0 +1 @@
1
+ import {a,b}from'../chunk-YDBHIXG5.mjs';import {b as b$1,a as a$1}from'../chunk-6E7DD34Z.mjs';function R(){return performance.timeOrigin+performance.now()}function _(e){let t=crypto.getRandomValues(new Uint8Array(e)),r="";for(let o of t)r+=o.toString(16).padStart(2,"0");return /^0+$/.test(r)?_(e):r}var x=()=>_(16),y=()=>_(8);var C=1e3,j=8e3;function X(e){return e instanceof Error?{"exception.type":e.name,"exception.message":e.message.slice(0,C),"exception.stacktrace":e.stack?.slice(0,j)}:{"exception.type":"Error","exception.message":String(e).slice(0,C)}}var Y=new RegExp(["Loading (CSS )?chunk \\S+ failed","Failed to fetch dynamically imported module","Importing a module script failed","error loading dynamically imported module"].join("|"),"i");function J(e){return e instanceof Error?e.name==="ChunkLoadError"||Y.test(e.message):false}var z=/Minified React error #(418|423|425)\b|Hydration failed|error while hydrating|did not match\. Server/i;function Q(e,t){return J(t)?b.CHUNK_ERROR:t instanceof Error&&z.test(t.message)?b.HYDRATION_ERROR:e}function v(e){function t(o,n,i={}){try{let a=R();e.emit({name:o,traceId:e.traceId,spanId:y(),parentSpanId:e.parentSpanId,startMs:a,endMs:a,...i,attributes:{"page.path":location.pathname,...n}});}catch{}}function r(o,n,i={},a={}){try{let c={name:"exception",timeMs:R(),attributes:X(n)};t(Q(o,n),i,{...a,error:!0,events:[c]});}catch{}}return {record:t,recordException:r}}var U=e=>e.split(/[?#]/)[0];function Z(e){if(e instanceof HTMLImageElement||e instanceof HTMLScriptElement)return e.src;if(e instanceof HTMLLinkElement)return e.href}function V(e){let{record:t,recordException:r}=v(e),o=e.slowFetchMs??2e3;function n(d){if(d instanceof ErrorEvent){r(b.JS_ERROR,d.error??d.message);return}let p=Z(d.target);if(p===void 0)return;let u=d.target.tagName.toLowerCase(),s=u!=="img"&&new URL(p,location.href).origin!==location.origin;t(b.RESOURCE_ERROR,{"resource.tag":u,"resource.url":U(p),"resource.third_party":s||void 0},{error:!s});}function i(d){r(b.UNHANDLED_REJECTION,d.reason);}let a=new URL(e.relayUrl,location.href).href,c=window.fetch,m=c.bind(window);function g(d,p){try{let u=d instanceof Request?d:void 0,s=new URL(u?.url??String(d),location.href),l=U(s.href);return l===a?void 0:{method:(p?.method??u?.method??"GET").toUpperCase(),url:l,sameOrigin:s.origin===location.origin}}catch{return}}function h(d,p,u){try{let s=p?.headers??(d instanceof Request?d.headers:{}),l=new Headers(s);if(l.has("traceparent"))return p;let S=l.has("next-router-prefetch")?"00":"01";return l.set("traceparent",`00-${e.traceId}-${u}-${S}`),{...p,headers:l}}catch{return p}}async function E(d,p){let u=g(d,p);if(!u)return m(d,p);let s=`${b.FETCH} ${u.method}`,l={"http.method":u.method,"url.full":u.url},S=y(),w={kind:"client",startMs:R(),spanId:S};u.sameOrigin&&(p=h(d,p,S));try{let b=await m(d,p),A={...l,"http.status_code":b.status};return !b.ok&&b.type!=="opaque"?t(s,A,{...w,error:b.status>=500}):b.ok&&u.sameOrigin&&R()-w.startMs>=o&&t(s,A,{...w,error:!1}),b}catch(b){throw b instanceof DOMException&&b.name==="AbortError"||r(s,b,l,w),b}}return window.addEventListener("error",n,true),window.addEventListener("unhandledrejection",i),window.fetch=E,()=>{window.removeEventListener("error",n,true),window.removeEventListener("unhandledrejection",i),window.fetch===E&&(window.fetch=c);}}function ee(e){let t=e.events?.find(n=>n.name==="exception")?.attributes;if(!t)return;let{"exception.type":r,"exception.message":o}=t;return `${r}|${o}|${String(t["exception.stacktrace"]??"").slice(0,500)}`}function D(e=Date.now){let t=new Map;return function(o){let n=ee(o);if(n===void 0)return false;let i=e(),a=o.attributes?.["component.id"]!==void 0,c=t.get(n),m=c!==void 0&&i-c.at<1e3;return m&&(!a||c.boundary)?true:(t.size>=100&&t.clear(),t.set(n,{at:i,boundary:a||m&&c.boundary}),false)}}function P(){try{let e=window;e[b$1]=!0;let t=e[a$1];return delete e[a$1],t?.take()??[]}catch{return []}}function H(e,t){for(let r of t){let o={startMs:r.ts,endMs:r.ts};if(r.k==="r"){let n={"resource.tag":r.tag,"resource.url":r.url};e.record(b.RESOURCE_ERROR,n,{...o,error:true});}else {let n=new Error(r.message);n.name=r.type??"Error",n.stack=r.stack;let i=r.k==="u"?b.UNHANDLED_REJECTION:b.JS_ERROR;e.recordException(i,n,{},o);}}}var te=1e3,re=3,ne="input, textarea, select, [contenteditable]",oe="button, a, [role=button], summary, label";function ie(e){let t=[],r=e;for(let o=0;r&&r!==document.body&&o<3;o++){let n=r.tagName.toLowerCase();if(r.id){t.unshift(`${n}#${r.id}`);break}let i=Array.from(r.classList).slice(0,2);t.unshift(i.length?`${n}.${i.join(".")}`:n),r=r.parentElement;}return t.join(" > ").slice(0,200)}function F(e){let t=null,r=[],o=false;function n(i){try{let a=i.target instanceof Element?i.target:null;if(!a||a===document.body||a===document.documentElement||a.closest(ne))return;let c=a.closest(oe)??a,m=Date.now();c!==t&&(t=c,r=[]),r=r.filter(g=>m-g<te),r.push(m),r.length===1&&(o=!1),r.length>=re&&!o&&(o=!0,e(b.RAGE_CLICK,{"ui.target":ie(c)}));}catch{}}return document.addEventListener("click",n,true),()=>document.removeEventListener("click",n,true)}var $=Symbol.for("@shopkit/observability.reporter");function M(e){globalThis[$]=e;}function B(){return globalThis[$]}var ae={internal:1,client:3},se=0,ce=2;function O(e){let t=Math.floor(e),r=Math.round((e-t)*1e6);return r===1e6&&(t+=1,r=0),t===0?String(r):`${t}${String(r).padStart(6,"0")}`}function ue(e){if(typeof e=="string")return {stringValue:e};if(typeof e=="boolean")return {boolValue:e};if(Number.isFinite(e))return Number.isInteger(e)?{intValue:String(e)}:{doubleValue:e}}function N(e){let t=[];if(!e)return t;for(let r in e){let o=e[r],n=o===void 0?void 0:ue(o);n&&t.push({key:r,value:n});}return t}function de(e){return {traceId:e.traceId,spanId:e.spanId,...e.parentSpanId&&{parentSpanId:e.parentSpanId},name:e.name,kind:ae[e.kind??"internal"],startTimeUnixNano:O(e.startMs),endTimeUnixNano:O(e.endMs),attributes:N(e.attributes),events:(e.events??[]).map(t=>({name:t.name,timeUnixNano:O(t.timeMs),attributes:N(t.attributes)})),status:{code:e.error?ce:se}}}function K(e,t,r){return {resourceSpans:[{resource:{attributes:N(e)},scopeSpans:[{scope:t,spans:r.map(de)}]}]}}var pe=6e4,G="text/plain;charset=UTF-8";function q(e){let{url:t,resource:r,scope:o}=e,n=e.maxBatch??30,i=e.flushMs??5e3,a=e.sessionCap??100,c=[],m=0,g=0,h=0,E;function d(){if(g===h)return;let s=R(),l=g-h;return h=g,{name:b.TELEMETRY_DROPPED,traceId:e.traceId??x(),spanId:y(),startMs:s,endMs:s,attributes:{"telemetry.dropped_count":l,"page.path":location.pathname}}}function p(s,l){let S=JSON.stringify(K(r,o,s)),w=new Blob([S],{type:G});if(w.size>pe){if(s.length===1)return;let b=s.length>>1;p(s.slice(0,b),l),p(s.slice(b),l);return}l&&navigator.sendBeacon?.(t,w)||fetch(t,{method:"POST",body:S,headers:{"content-type":G},keepalive:l,credentials:"omit"}).catch(()=>{});}function u(s){clearTimeout(E),E=void 0;let l=s?d():void 0;if(l&&c.push(l),c.length===0)return;let S=c;c=[];try{p(S,s);}catch{}}return document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&u(true);}),window.addEventListener("pagehide",()=>u(true)),{add(s){if(m>=a){g++;return}m++,c.push(s),document.visibilityState==="hidden"?u(true):c.length>=n?u(false):E??(E=setTimeout(()=>u(false),i));}}}function W(e){let t=false;function r(n){if(t)return;let i=R();try{e.emit({name:b.WEB_VITAL,traceId:e.traceId,spanId:y(),parentSpanId:e.parentSpanId,startMs:i,endMs:i,attributes:{"page.path":location.pathname,"web_vital.name":n.name,"web_vital.value":n.value,"web_vital.rating":n.rating,"web_vital.navigation_type":n.navigationType}});}catch{}}function o(){import('web-vitals').then(({onCLS:n,onFCP:i,onINP:a,onLCP:c,onTTFB:m})=>{[n,i,a,c,m].forEach(g=>g(r));}).catch(()=>{});}return document.readyState==="complete"?o():window.addEventListener("load",o,{once:true}),()=>{t=true,window.removeEventListener("load",o);}}var k=Symbol.for("@shopkit/observability"),le=/^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;function fe(e){let t=e?le.exec(e.trim()):null;if(!(!t||/^0+$/.test(t[1])||/^0+$/.test(t[2])))return {traceId:t[1],parentSpanId:t[2]}}function me(){return document.cookie.split(";").map(e=>e.split("=")[0].trim())}function ge(e){if(e?.paths?.some(o=>o.test(location.pathname)))return true;let t=new URLSearchParams(location.search);if(e?.params?.some(o=>t.has(o)))return true;let r=e?.cookies?.length?me():[];return e?.cookies?.some(o=>r.includes(o))??false}function be(e){return String(e.events?.[0]?.attributes?.["exception.message"]??"")}function Re(e){try{return Se(e)}catch{return ()=>{}}}function Se(e){let t=globalThis;if(typeof window>"u")return ()=>{};let r=P();if(t[k]||ge(e.ignore))return ()=>{};t[k]=true;let o=document.querySelector('meta[name="traceparent"]')?.getAttribute("content"),n=fe(o)?.traceId,i={traceId:x()},a$1=q({url:e.relayUrl,resource:{[a.SERVICE_NAME]:e.serviceName,[a.APP]:e.app,[a.SERVICE_VERSION]:e.release,[a.DEPLOYMENT_ENVIRONMENT]:e.environment},scope:{name:"@shopkit/observability",version:"0.1.0"},sessionCap:e.sessionCap,traceId:i.traceId}),c=e.ignore?.errors??[],m=D(),g=u=>{let s=be(u);s&&c.some(l=>l.test(s))||m(u)||(u.attributes={...u.attributes,"page.type":e.pageType?.(location.pathname),"page.render_trace_id":n,"network.effective_type":navigator.connection?.effectiveType},a$1.add(u));},h=V({emit:g,...i,relayUrl:e.relayUrl,slowFetchMs:e.slowFetchMs}),E=W({emit:g,...i}),d=v({emit:g,...i});M(d),H(d,r);let p=F(d.record);return ()=>{h(),E(),p(),M(void 0),t[k]=false;}}var Ee=1e3;function ye(e,t){let r=R(),o=false;function n(i,a){if(!o){o=true;try{B()?.record(b.FLOW_PREFIX+e,{...t,...a,"flow.outcome":i},{startMs:r,error:i==="failure"});}catch{}}}return {success:i=>n("success",{...i}),fail:(i,a,c)=>n("failure",{...c,"flow.failure_reason":i,"flow.error_message":a===void 0?void 0:(a instanceof Error?a.message:String(a)).slice(0,Ee)})}}export{Re as initObservability,ye as startFlow};
@@ -0,0 +1,34 @@
1
+ import { App } from '../index.mjs';
2
+
3
+ /** Global the stub publishes; initObservability takes the queue from it. */
4
+ declare const HEAD_STUB_GLOBAL = "__shopkitObsStub";
5
+ /**
6
+ * Set by initObservability. Next puts its async chunk scripts above the layout's
7
+ * <head> content, so a cached chunk can start the app before the stub runs; the
8
+ * stub must then stay off, or it would report a healthy page as never started.
9
+ */
10
+ declare const BOOTED_GLOBAL = "__shopkitObsBooted";
11
+ /** An error the stub caught before the app started. */
12
+ interface HeadStubEntry {
13
+ /** j: uncaught error, u: unhandled rejection, r: failed <img>/<script>/<link> */
14
+ k: "j" | "u" | "r";
15
+ ts: number;
16
+ type?: string;
17
+ message?: string;
18
+ stack?: string;
19
+ tag?: string;
20
+ url?: string;
21
+ }
22
+
23
+ interface HeadStubConfig {
24
+ relayUrl: string;
25
+ serviceName: string;
26
+ app: App;
27
+ }
28
+ /**
29
+ * Inline script for the first child of <head>. Catches errors before the app's JS
30
+ * loads, and reports by itself when the app never starts (blank page).
31
+ */
32
+ declare function headStubScript(config: HeadStubConfig, graceMs?: number): string;
33
+
34
+ export { BOOTED_GLOBAL, HEAD_STUB_GLOBAL, type HeadStubConfig, type HeadStubEntry, headStubScript };
@@ -0,0 +1,34 @@
1
+ import { App } from '../index.js';
2
+
3
+ /** Global the stub publishes; initObservability takes the queue from it. */
4
+ declare const HEAD_STUB_GLOBAL = "__shopkitObsStub";
5
+ /**
6
+ * Set by initObservability. Next puts its async chunk scripts above the layout's
7
+ * <head> content, so a cached chunk can start the app before the stub runs; the
8
+ * stub must then stay off, or it would report a healthy page as never started.
9
+ */
10
+ declare const BOOTED_GLOBAL = "__shopkitObsBooted";
11
+ /** An error the stub caught before the app started. */
12
+ interface HeadStubEntry {
13
+ /** j: uncaught error, u: unhandled rejection, r: failed <img>/<script>/<link> */
14
+ k: "j" | "u" | "r";
15
+ ts: number;
16
+ type?: string;
17
+ message?: string;
18
+ stack?: string;
19
+ tag?: string;
20
+ url?: string;
21
+ }
22
+
23
+ interface HeadStubConfig {
24
+ relayUrl: string;
25
+ serviceName: string;
26
+ app: App;
27
+ }
28
+ /**
29
+ * Inline script for the first child of <head>. Catches errors before the app's JS
30
+ * loads, and reports by itself when the app never starts (blank page).
31
+ */
32
+ declare function headStubScript(config: HeadStubConfig, graceMs?: number): string;
33
+
34
+ export { BOOTED_GLOBAL, HEAD_STUB_GLOBAL, type HeadStubConfig, type HeadStubEntry, headStubScript };
@@ -0,0 +1,40 @@
1
+ 'use strict';var r="__shopkitObsStub",n="__shopkitObsBooted";var o=`
2
+ // C = config: u relay URL, s service name, a app, g grace ms, v package version.
3
+ // w window, q early errors, b app took over, d report sent, T grace timer.
4
+ if(window.${n})return;
5
+ var w=window,q=[],b=false,d=false,T;
6
+ // N: now. P: queue an entry (at most 20).
7
+ function N(){return Date.now()}
8
+ function P(x){if(q.length<20)q.push(x)}
9
+ // E: uncaught error, or a failed <img>/<script>/<link> (needs the capture phase).
10
+ function E(e){try{var t=e.target;
11
+ if(t&&t!==w&&t.tagName){P({k:"r",ts:N(),tag:t.tagName.toLowerCase(),url:String(t.currentSrc||t.src||t.href||"").split(/[?#]/)[0]});return}
12
+ var x=e.error;P({k:"j",ts:N(),type:(x&&x.name)||"Error",message:String((x&&x.message)||e.message),stack:x&&x.stack})}catch(_){}}
13
+ // R: unhandled promise rejection.
14
+ function R(e){try{var r=e.reason;P({k:"u",ts:N(),type:(r&&r.name)||"Error",message:String((r&&r.message)||r),stack:r&&r.stack})}catch(_){}}
15
+ // X: stop listening.
16
+ function X(){w.removeEventListener("error",E,true);w.removeEventListener("unhandledrejection",R);w.removeEventListener("pagehide",F);clearTimeout(T)}
17
+ // H: random hex id. K: OTLP string attribute, short so 20 entries fit one 64 KB beacon.
18
+ // S: OTLP span (always an error).
19
+ function H(n){var a=new Uint8Array(n),s="";crypto.getRandomValues(a);for(var i=0;i<n;i++)s+=(a[i]|256).toString(16).slice(1);return s}
20
+ function K(k,v){return{key:k,value:{stringValue:String(v).slice(0,1000)}}}
21
+ function S(name,trace,ts,attrs,ex){var n=ts+"000000";return{traceId:trace,spanId:H(8),name:name,kind:1,startTimeUnixNano:n,endTimeUnixNano:n,attributes:[K("page.path",location.pathname)].concat(attrs),events:ex?[{name:"exception",timeUnixNano:n,attributes:ex}]:[],status:{code:2}}}
22
+ // F: the app never took over: send app_not_started plus the queue, by beacon.
23
+ function F(){if(b||d)return;d=true;X();try{
24
+ var tr=H(16),sp=[S("browser.app_not_started",tr,N(),[])];
25
+ for(var i=0;i<q.length;i++){var x=q[i];
26
+ if(x.k==="r")sp.push(S("browser.resource_error",tr,x.ts,[K("resource.tag",x.tag),K("resource.url",x.url)]));
27
+ else sp.push(S(x.k==="u"?"browser.unhandled_rejection":"browser.js_error",tr,x.ts,[],[K("exception.type",x.type),K("exception.message",x.message)].concat(x.stack?[K("exception.stacktrace",x.stack)]:[])))}
28
+ var body=JSON.stringify({resourceSpans:[{resource:{attributes:[K("service.name",C.s),K("app",C.a)]},scopeSpans:[{scope:{name:"@shopkit/observability",version:C.v},spans:sp}]}]});
29
+ var ty="text/plain;charset=UTF-8";
30
+ if(!(navigator.sendBeacon&&navigator.sendBeacon(C.u,new Blob([body],{type:ty}))))fetch(C.u,{method:"POST",body:body,keepalive:true,credentials:"omit",headers:{"content-type":ty}})["catch"](function(){});
31
+ }catch(_){}}
32
+ w.addEventListener("error",E,true);
33
+ w.addEventListener("unhandledrejection",R);
34
+ // Only after load can "never started" be told apart from "still loading".
35
+ w.addEventListener("load",function(){T=setTimeout(F,C.g);w.addEventListener("pagehide",F)});
36
+ // initObservability calls take(): the app started, hand the queue over.
37
+ w.${r}={take:function(){b=true;X();return q}};
38
+ `,i=t=>t.split(`
39
+ `).map(e=>e.trim()).filter(e=>e&&!e.startsWith("//")).join(""),c=i(o);function d(t,e=3e3){let a={u:t.relayUrl,s:t.serviceName,a:t.app,g:e,v:"0.1.0"},s=JSON.stringify(a).replace(/</g,"\\u003c");return `(function(C){${c}})(${s});`}
40
+ exports.BOOTED_GLOBAL=n;exports.HEAD_STUB_GLOBAL=r;exports.headStubScript=d;
@@ -0,0 +1,40 @@
1
+ import {b,a}from'../chunk-6E7DD34Z.mjs';export{b as BOOTED_GLOBAL,a as HEAD_STUB_GLOBAL}from'../chunk-6E7DD34Z.mjs';var o=`
2
+ // C = config: u relay URL, s service name, a app, g grace ms, v package version.
3
+ // w window, q early errors, b app took over, d report sent, T grace timer.
4
+ if(window.${b})return;
5
+ var w=window,q=[],b=false,d=false,T;
6
+ // N: now. P: queue an entry (at most 20).
7
+ function N(){return Date.now()}
8
+ function P(x){if(q.length<20)q.push(x)}
9
+ // E: uncaught error, or a failed <img>/<script>/<link> (needs the capture phase).
10
+ function E(e){try{var t=e.target;
11
+ if(t&&t!==w&&t.tagName){P({k:"r",ts:N(),tag:t.tagName.toLowerCase(),url:String(t.currentSrc||t.src||t.href||"").split(/[?#]/)[0]});return}
12
+ var x=e.error;P({k:"j",ts:N(),type:(x&&x.name)||"Error",message:String((x&&x.message)||e.message),stack:x&&x.stack})}catch(_){}}
13
+ // R: unhandled promise rejection.
14
+ function R(e){try{var r=e.reason;P({k:"u",ts:N(),type:(r&&r.name)||"Error",message:String((r&&r.message)||r),stack:r&&r.stack})}catch(_){}}
15
+ // X: stop listening.
16
+ function X(){w.removeEventListener("error",E,true);w.removeEventListener("unhandledrejection",R);w.removeEventListener("pagehide",F);clearTimeout(T)}
17
+ // H: random hex id. K: OTLP string attribute, short so 20 entries fit one 64 KB beacon.
18
+ // S: OTLP span (always an error).
19
+ function H(n){var a=new Uint8Array(n),s="";crypto.getRandomValues(a);for(var i=0;i<n;i++)s+=(a[i]|256).toString(16).slice(1);return s}
20
+ function K(k,v){return{key:k,value:{stringValue:String(v).slice(0,1000)}}}
21
+ function S(name,trace,ts,attrs,ex){var n=ts+"000000";return{traceId:trace,spanId:H(8),name:name,kind:1,startTimeUnixNano:n,endTimeUnixNano:n,attributes:[K("page.path",location.pathname)].concat(attrs),events:ex?[{name:"exception",timeUnixNano:n,attributes:ex}]:[],status:{code:2}}}
22
+ // F: the app never took over: send app_not_started plus the queue, by beacon.
23
+ function F(){if(b||d)return;d=true;X();try{
24
+ var tr=H(16),sp=[S("browser.app_not_started",tr,N(),[])];
25
+ for(var i=0;i<q.length;i++){var x=q[i];
26
+ if(x.k==="r")sp.push(S("browser.resource_error",tr,x.ts,[K("resource.tag",x.tag),K("resource.url",x.url)]));
27
+ else sp.push(S(x.k==="u"?"browser.unhandled_rejection":"browser.js_error",tr,x.ts,[],[K("exception.type",x.type),K("exception.message",x.message)].concat(x.stack?[K("exception.stacktrace",x.stack)]:[])))}
28
+ var body=JSON.stringify({resourceSpans:[{resource:{attributes:[K("service.name",C.s),K("app",C.a)]},scopeSpans:[{scope:{name:"@shopkit/observability",version:C.v},spans:sp}]}]});
29
+ var ty="text/plain;charset=UTF-8";
30
+ if(!(navigator.sendBeacon&&navigator.sendBeacon(C.u,new Blob([body],{type:ty}))))fetch(C.u,{method:"POST",body:body,keepalive:true,credentials:"omit",headers:{"content-type":ty}})["catch"](function(){});
31
+ }catch(_){}}
32
+ w.addEventListener("error",E,true);
33
+ w.addEventListener("unhandledrejection",R);
34
+ // Only after load can "never started" be told apart from "still loading".
35
+ w.addEventListener("load",function(){T=setTimeout(F,C.g);w.addEventListener("pagehide",F)});
36
+ // initObservability calls take(): the app started, hand the queue over.
37
+ w.${a}={take:function(){b=true;X();return q}};
38
+ `,i=t=>t.split(`
39
+ `).map(e=>e.trim()).filter(e=>e&&!e.startsWith("//")).join(""),c=i(o);function u(t,e=3e3){let a={u:t.relayUrl,s:t.serviceName,a:t.app,g:e,v:"0.1.0"},s=JSON.stringify(a).replace(/</g,"\\u003c");return `(function(C){${c}})(${s});`}
40
+ export{u as headStubScript};
@@ -0,0 +1,23 @@
1
+ declare const RESOURCE_ATTR: {
2
+ readonly SERVICE_NAME: "service.name";
3
+ readonly SERVICE_VERSION: "service.version";
4
+ readonly DEPLOYMENT_ENVIRONMENT: "deployment.environment";
5
+ readonly APP: "app";
6
+ readonly MERCHANT_ID: "merchant.id";
7
+ };
8
+ declare const SPAN: {
9
+ readonly JS_ERROR: "browser.js_error";
10
+ readonly UNHANDLED_REJECTION: "browser.unhandled_rejection";
11
+ readonly RESOURCE_ERROR: "browser.resource_error";
12
+ readonly RENDER_ERROR: "browser.render_error";
13
+ readonly HYDRATION_ERROR: "browser.hydration_error";
14
+ readonly CHUNK_ERROR: "browser.chunk_error";
15
+ readonly FETCH: "browser.fetch";
16
+ readonly WEB_VITAL: "web_vital";
17
+ readonly RAGE_CLICK: "ui.rage_click";
18
+ readonly TELEMETRY_DROPPED: "telemetry.dropped";
19
+ readonly FLOW_PREFIX: "flow.";
20
+ };
21
+ type App = "storefront" | "editor" | "admin";
22
+
23
+ export { type App, RESOURCE_ATTR, SPAN };
@@ -0,0 +1,23 @@
1
+ declare const RESOURCE_ATTR: {
2
+ readonly SERVICE_NAME: "service.name";
3
+ readonly SERVICE_VERSION: "service.version";
4
+ readonly DEPLOYMENT_ENVIRONMENT: "deployment.environment";
5
+ readonly APP: "app";
6
+ readonly MERCHANT_ID: "merchant.id";
7
+ };
8
+ declare const SPAN: {
9
+ readonly JS_ERROR: "browser.js_error";
10
+ readonly UNHANDLED_REJECTION: "browser.unhandled_rejection";
11
+ readonly RESOURCE_ERROR: "browser.resource_error";
12
+ readonly RENDER_ERROR: "browser.render_error";
13
+ readonly HYDRATION_ERROR: "browser.hydration_error";
14
+ readonly CHUNK_ERROR: "browser.chunk_error";
15
+ readonly FETCH: "browser.fetch";
16
+ readonly WEB_VITAL: "web_vital";
17
+ readonly RAGE_CLICK: "ui.rage_click";
18
+ readonly TELEMETRY_DROPPED: "telemetry.dropped";
19
+ readonly FLOW_PREFIX: "flow.";
20
+ };
21
+ type App = "storefront" | "editor" | "admin";
22
+
23
+ export { type App, RESOURCE_ATTR, SPAN };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var r={SERVICE_NAME:"service.name",SERVICE_VERSION:"service.version",DEPLOYMENT_ENVIRONMENT:"deployment.environment",APP:"app",MERCHANT_ID:"merchant.id"},e={JS_ERROR:"browser.js_error",UNHANDLED_REJECTION:"browser.unhandled_rejection",RESOURCE_ERROR:"browser.resource_error",RENDER_ERROR:"browser.render_error",HYDRATION_ERROR:"browser.hydration_error",CHUNK_ERROR:"browser.chunk_error",FETCH:"browser.fetch",WEB_VITAL:"web_vital",RAGE_CLICK:"ui.rage_click",TELEMETRY_DROPPED:"telemetry.dropped",FLOW_PREFIX:"flow."};exports.RESOURCE_ATTR=r;exports.SPAN=e;
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export{a as RESOURCE_ATTR,b as SPAN}from'./chunk-YDBHIXG5.mjs';
@@ -0,0 +1,39 @@
1
+ import { Component, ReactNode, ErrorInfo } from 'react';
2
+
3
+ interface ObservabilityBoundaryProps {
4
+ /** What failed, for dashboards: a storefront widget, an editor panel, a route. */
5
+ kind: "widget" | "panel" | "route";
6
+ id: string;
7
+ type?: string;
8
+ /** Shown instead of the failed subtree; defaults to nothing. */
9
+ fallback?: ReactNode;
10
+ children?: ReactNode;
11
+ }
12
+ /**
13
+ * Contains a render crash to its subtree (the rest of the page stays up) and reports
14
+ * it, named, as a browser.render_error span. Plain React: works in any app.
15
+ */
16
+ declare class ObservabilityBoundary extends Component<ObservabilityBoundaryProps, {
17
+ failed: boolean;
18
+ }> {
19
+ state: {
20
+ failed: boolean;
21
+ };
22
+ static getDerivedStateFromError(): {
23
+ failed: boolean;
24
+ };
25
+ componentDidCatch(error: Error, info: ErrorInfo): void;
26
+ render(): ReactNode;
27
+ }
28
+
29
+ /**
30
+ * For an error page or fallback that receives an error another boundary caught (e.g. a
31
+ * route's error page): reports it once as a browser.render_error. React doesn't pass
32
+ * caught errors to the page's global error handlers, so they're otherwise unseen.
33
+ * `digest` (set when the error came from the server) matches the server's log line.
34
+ */
35
+ declare function useReportError(error: Error & {
36
+ digest?: string;
37
+ }, id: string): void;
38
+
39
+ export { ObservabilityBoundary, type ObservabilityBoundaryProps, useReportError };
@@ -0,0 +1,39 @@
1
+ import { Component, ReactNode, ErrorInfo } from 'react';
2
+
3
+ interface ObservabilityBoundaryProps {
4
+ /** What failed, for dashboards: a storefront widget, an editor panel, a route. */
5
+ kind: "widget" | "panel" | "route";
6
+ id: string;
7
+ type?: string;
8
+ /** Shown instead of the failed subtree; defaults to nothing. */
9
+ fallback?: ReactNode;
10
+ children?: ReactNode;
11
+ }
12
+ /**
13
+ * Contains a render crash to its subtree (the rest of the page stays up) and reports
14
+ * it, named, as a browser.render_error span. Plain React: works in any app.
15
+ */
16
+ declare class ObservabilityBoundary extends Component<ObservabilityBoundaryProps, {
17
+ failed: boolean;
18
+ }> {
19
+ state: {
20
+ failed: boolean;
21
+ };
22
+ static getDerivedStateFromError(): {
23
+ failed: boolean;
24
+ };
25
+ componentDidCatch(error: Error, info: ErrorInfo): void;
26
+ render(): ReactNode;
27
+ }
28
+
29
+ /**
30
+ * For an error page or fallback that receives an error another boundary caught (e.g. a
31
+ * route's error page): reports it once as a browser.render_error. React doesn't pass
32
+ * caught errors to the page's global error handlers, so they're otherwise unseen.
33
+ * `digest` (set when the error came from the server) matches the server's log line.
34
+ */
35
+ declare function useReportError(error: Error & {
36
+ digest?: string;
37
+ }, id: string): void;
38
+
39
+ export { ObservabilityBoundary, type ObservabilityBoundaryProps, useReportError };
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ "use strict";var R=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var _=(e,r)=>{for(var o in r)R(e,o,{get:r[o],enumerable:!0})},u=(e,r,o,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of f(r))!b.call(e,t)&&t!==o&&R(e,t,{get:()=>r[t],enumerable:!(n=m(r,t))||n.enumerable});return e};var y=e=>u(R({},"__esModule",{value:!0}),e);var h={};_(h,{ObservabilityBoundary:()=>s,useReportError:()=>a});module.exports=y(h);var c=require("react");var i={JS_ERROR:"browser.js_error",UNHANDLED_REJECTION:"browser.unhandled_rejection",RESOURCE_ERROR:"browser.resource_error",RENDER_ERROR:"browser.render_error",HYDRATION_ERROR:"browser.hydration_error",CHUNK_ERROR:"browser.chunk_error",FETCH:"browser.fetch",WEB_VITAL:"web_vital",RAGE_CLICK:"ui.rage_click",TELEMETRY_DROPPED:"telemetry.dropped",FLOW_PREFIX:"flow."};var N=Symbol.for("@shopkit/observability.reporter");function O(){return globalThis[N]}function p(e,r,o){try{O()?.recordException(e,r,o)}catch{}}var T=4e3,s=class extends c.Component{constructor(){super(...arguments);this.state={failed:!1}}static getDerivedStateFromError(){return{failed:!0}}componentDidCatch(o,n){let{kind:t,id:d,type:l}=this.props;p(i.RENDER_ERROR,o,{"component.kind":t,"component.id":d,"component.type":l,"react.component_stack":n.componentStack?.slice(0,T)})}render(){return this.state.failed?this.props.fallback??null:this.props.children}};var E=require("react");function a(e,r){(0,E.useEffect)(()=>{p(i.RENDER_ERROR,e,{"component.kind":"route","component.id":r,"error.digest":e.digest})},[e,r])}0&&(module.exports={ObservabilityBoundary,useReportError});
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ import{Component as d}from"react";var o={JS_ERROR:"browser.js_error",UNHANDLED_REJECTION:"browser.unhandled_rejection",RESOURCE_ERROR:"browser.resource_error",RENDER_ERROR:"browser.render_error",HYDRATION_ERROR:"browser.hydration_error",CHUNK_ERROR:"browser.chunk_error",FETCH:"browser.fetch",WEB_VITAL:"web_vital",RAGE_CLICK:"ui.rage_click",TELEMETRY_DROPPED:"telemetry.dropped",FLOW_PREFIX:"flow."};var E=Symbol.for("@shopkit/observability.reporter");function a(){return globalThis[E]}function t(r,e,n){try{a()?.recordException(r,e,n)}catch{}}var l=4e3,i=class extends d{constructor(){super(...arguments);this.state={failed:!1}}static getDerivedStateFromError(){return{failed:!0}}componentDidCatch(n,p){let{kind:s,id:R,type:c}=this.props;t(o.RENDER_ERROR,n,{"component.kind":s,"component.id":R,"component.type":c,"react.component_stack":p.componentStack?.slice(0,l)})}render(){return this.state.failed?this.props.fallback??null:this.props.children}};import{useEffect as m}from"react";function f(r,e){m(()=>{t(o.RENDER_ERROR,r,{"component.kind":"route","component.id":e,"error.digest":r.digest})},[r,e])}export{i as ObservabilityBoundary,f as useReportError};
@@ -0,0 +1,39 @@
1
+ import { RawSourceMap } from 'source-map-js';
2
+
3
+ /** Returns the source map for a script URL, or undefined when there is none. */
4
+ type SourceMapLookup = (scriptUrl: string) => Promise<RawSourceMap | undefined>;
5
+ /**
6
+ * Maps on disk: scripts served under `urlPath` have their maps in `dir`, at the same
7
+ * relative path plus ".map" (the layout `shopkit-move-sourcemaps` writes). Node only.
8
+ * e.g. { urlPath: "/_next/static/", dir: ".next/obs-sourcemaps" }
9
+ */
10
+ declare function fileSourceMaps(o: {
11
+ urlPath: string;
12
+ dir: string;
13
+ }): SourceMapLookup;
14
+
15
+ interface RelayOptions {
16
+ /** Collector OTLP/HTTP traces endpoint, e.g. http://collector:4318/v1/traces. */
17
+ endpoint: string;
18
+ /** Extra collector headers, e.g. an ingestion key. */
19
+ headers?: Record<string, string>;
20
+ /** Resource attributes set by the server; they overwrite what the browser sent. */
21
+ trusted: Record<string, string | undefined>;
22
+ /** Resolves minified stack traces. Create it once and reuse it, so parsed maps are cached. */
23
+ sourceMaps?: SourceMapLookup;
24
+ }
25
+ type RelayStatus = 204 | 400 | 413 | 502;
26
+ declare const MAX_BODY_BYTES: number;
27
+ /**
28
+ * Validates, scrubs and stamps one browser batch, then forwards it to the collector.
29
+ * Collector errors are never echoed back to the browser.
30
+ */
31
+ declare function processBatch(body: string, options: RelayOptions): Promise<RelayStatus>;
32
+
33
+ /**
34
+ * Web-standard POST handler for the browser relay, e.g. a Next.js route:
35
+ * `export const POST = createRelayHandler({ endpoint, trusted })`.
36
+ */
37
+ declare function createRelayHandler(options: RelayOptions): (req: Request) => Promise<Response>;
38
+
39
+ export { MAX_BODY_BYTES, type RelayOptions, type RelayStatus, type SourceMapLookup, createRelayHandler, fileSourceMaps, processBatch };
@@ -0,0 +1,39 @@
1
+ import { RawSourceMap } from 'source-map-js';
2
+
3
+ /** Returns the source map for a script URL, or undefined when there is none. */
4
+ type SourceMapLookup = (scriptUrl: string) => Promise<RawSourceMap | undefined>;
5
+ /**
6
+ * Maps on disk: scripts served under `urlPath` have their maps in `dir`, at the same
7
+ * relative path plus ".map" (the layout `shopkit-move-sourcemaps` writes). Node only.
8
+ * e.g. { urlPath: "/_next/static/", dir: ".next/obs-sourcemaps" }
9
+ */
10
+ declare function fileSourceMaps(o: {
11
+ urlPath: string;
12
+ dir: string;
13
+ }): SourceMapLookup;
14
+
15
+ interface RelayOptions {
16
+ /** Collector OTLP/HTTP traces endpoint, e.g. http://collector:4318/v1/traces. */
17
+ endpoint: string;
18
+ /** Extra collector headers, e.g. an ingestion key. */
19
+ headers?: Record<string, string>;
20
+ /** Resource attributes set by the server; they overwrite what the browser sent. */
21
+ trusted: Record<string, string | undefined>;
22
+ /** Resolves minified stack traces. Create it once and reuse it, so parsed maps are cached. */
23
+ sourceMaps?: SourceMapLookup;
24
+ }
25
+ type RelayStatus = 204 | 400 | 413 | 502;
26
+ declare const MAX_BODY_BYTES: number;
27
+ /**
28
+ * Validates, scrubs and stamps one browser batch, then forwards it to the collector.
29
+ * Collector errors are never echoed back to the browser.
30
+ */
31
+ declare function processBatch(body: string, options: RelayOptions): Promise<RelayStatus>;
32
+
33
+ /**
34
+ * Web-standard POST handler for the browser relay, e.g. a Next.js route:
35
+ * `export const POST = createRelayHandler({ endpoint, trusted })`.
36
+ */
37
+ declare function createRelayHandler(options: RelayOptions): (req: Request) => Promise<Response>;
38
+
39
+ export { MAX_BODY_BYTES, type RelayOptions, type RelayStatus, type SourceMapLookup, createRelayHandler, fileSourceMaps, processBatch };
@@ -0,0 +1,2 @@
1
+ 'use strict';var sourceMapJs=require('source-map-js');function p(e,t=()=>Date.now()){let n=t(),r=0;return function(){return t()-n>=6e4&&(n=t(),r=0),r++<e}}var h=/\?[^\s#)'"]*?(?=(?::\d+:\d+)?(?:[\s#)'"]|$))/g,w=/(?<![\w.+-])[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g,A=/(?<!\w)(?:\+?91[\s-]?)?[6-9]\d{9}(?!\w)/g;function M(e){return e.replace(/\S+/g,t=>{let n=t.indexOf("#");if(n<0||!t.slice(0,n).includes("://"))return t;let r=t.slice(n).search(/[)'"]/);return t.slice(0,n)+(r<0?"":t.slice(n+r))})}function f(e){return M(e.replace(h,"")).replace(w,"[email]").replace(A,"[phone]")}var k=/\.js:(\d+):(\d+)\)?$/;function m(e){let t=k.exec(e);if(!t)return;let n=e.indexOf("http"),r=e.slice(n,t.index+3);if(!(n<0||!/^https?:\/\//.test(r)))return {url:r,line:Number(t[1]),column:Number(t[2]),before:e.slice(0,n),after:t[0].endsWith(")")?")":""}}var O=30,v=p(30),g=new WeakMap;async function x(e,t){let n=g.get(e);if(n||g.set(e,n=new Map),n.has(t))return n.get(t);if(!v())return null;let r=null;try{let s=await e(t);s&&(r=new sourceMapJs.SourceMapConsumer(s));}catch{}return n.size>=O&&n.clear(),n.set(t,r),r}var P=e=>e.replace(/^webpack:\/\/[^/]*\//,"").replace(/^\.\//,"");async function y(e,t){let n=new Set;for(let s of e.match(/\S+/g)??[]){let o=m(s);o&&n.add(o.url);}let r=new Map;for(let s of n)r.set(s,await x(t,s));return e.replace(/\S+/g,s=>{let o=m(s),a=o&&r.get(o.url)?.originalPositionFor({line:o.line,column:o.column-1});return !o||!a?.source||!a.line?s:`${o.before}${P(a.source)}:${a.line}:${a.column+1}${o.after}`})}function B(e){return async t=>{let n=new URL(t).pathname;if(!n.startsWith(e.urlPath))return;let{readFile:r}=await import('fs/promises'),s=await import('path'),o=s.resolve(e.dir),a=decodeURIComponent(n.slice(e.urlPath.length)).replace(/^\/+/,""),c=s.resolve(o,`${a}.map`);if(c.startsWith(o+s.sep))return JSON.parse(await r(c,"utf8"))}}var u=64*1024,_=200,L=5e3,i=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function N(e){let t;try{t=JSON.parse(e);}catch{return}if(!i(t)||!Array.isArray(t.resourceSpans))return;let n=0;for(let r of t.resourceSpans){if(!i(r)||!Array.isArray(r.scopeSpans)||r.resource!==void 0&&!i(r.resource))return;for(let s of r.scopeSpans){if(!i(s)||!Array.isArray(s.spans)||!s.spans.every(i))return;n+=s.spans.length;}}return n>0&&n<=_?t:void 0}function l(e){if(Array.isArray(e))for(let t of e){let n=t?.value;typeof n?.stringValue=="string"&&(n.stringValue=f(n.stringValue));}}function T(e){if(typeof e.name=="string"&&(e.name=f(e.name)),l(e.attributes),!!Array.isArray(e.events))for(let t of e.events)i(t)&&l(t.attributes);}async function E(e,t){if(Array.isArray(e.events)){for(let n of e.events)if(!(!i(n)||!Array.isArray(n.attributes)))for(let r of n.attributes){let s=r?.value;if(!(r?.key!=="exception.stacktrace"||typeof s?.stringValue!="string"))try{s.stringValue=await y(s.stringValue,t);}catch{}}}}function C(e,t){let n=new Set(Object.keys(t)),r=e.resource?.attributes,s=Array.isArray(r)?r.filter(a=>i(a)&&!n.has(String(a.key))):[],o=Object.entries(t).filter(([,a])=>a!==void 0).map(([a,c])=>({key:a,value:{stringValue:c}}));e.resource={...e.resource,attributes:[...s,...o]};}async function d(e,t){if(new TextEncoder().encode(e).length>u)return 413;let n=N(e);if(!n)return 400;for(let r of n.resourceSpans){l(r.resource?.attributes),C(r,t.trusted);for(let s of r.scopeSpans)for(let o of s.spans)T(o),t.sourceMaps&&await E(o,t.sourceMaps);}try{return (await fetch(t.endpoint,{method:"POST",headers:{...t.headers,"content-type":"application/json"},body:JSON.stringify(n),signal:AbortSignal.timeout(L)})).ok?204:502}catch{return 502}}var V=/\+https?:\/\/|Googlebot|bingbot|Applebot|crawler|spider|HeadlessChrome|Lighthouse|facebookexternalhit|WhatsApp\//i,I=[["instagram",/\bInstagram\b/],["facebook",/\bFB(AN|AV|_IAB)\b/],["snapchat",/\bSnapchat\b/],["linkedin",/\bLinkedInApp\b/],["android_webview",/; wv\)/]],b=e=>V.test(e);function S(e){return {"device.type":/iPad|Tablet/i.test(e)?"tablet":/Mobi|Android|iPhone|iPod/i.test(e)?"mobile":"desktop","browser.in_app":I.find(([,t])=>t.test(e))?.[0]}}var W=6e3;async function D(e,t){if(!e.body)return "";let n=e.body.getReader(),r=new TextDecoder,s="",o=0;for(;;){let{done:a,value:c}=await n.read();if(a)return s+r.decode();if(o+=c.byteLength,o>t){n.cancel();return}s+=r.decode(c,{stream:true});}}function F(e){let t=p(W);async function n(r){if(Number(r.headers.get("content-length"))>u)return 413;if(!t())return 429;let s=r.headers.get("user-agent")??"";if(b(s))return 204;try{let o=await D(r,u);return o===void 0?413:await d(o,{...e,trusted:{...S(s),...e.trusted}})}catch{return 400}}return async function(s){return new Response(null,{status:await n(s)})}}
2
+ exports.MAX_BODY_BYTES=u;exports.createRelayHandler=F;exports.fileSourceMaps=B;exports.processBatch=d;
@@ -0,0 +1,2 @@
1
+ import {a}from'../chunk-ZNVBPNNQ.mjs';import {SourceMapConsumer}from'source-map-js';function p(e,t=()=>Date.now()){let r=t(),n=0;return function(){return t()-r>=6e4&&(r=t(),n=0),n++<e}}var w=/\.js:(\d+):(\d+)\)?$/;function m(e){let t=w.exec(e);if(!t)return;let r=e.indexOf("http"),n=e.slice(r,t.index+3);if(!(r<0||!/^https?:\/\//.test(n)))return {url:n,line:Number(t[1]),column:Number(t[2]),before:e.slice(0,r),after:t[0].endsWith(")")?")":""}}var A=30,M=p(30),y=new WeakMap;async function R(e,t){let r=y.get(e);if(r||y.set(e,r=new Map),r.has(t))return r.get(t);if(!M())return null;let n=null;try{let o=await e(t);o&&(n=new SourceMapConsumer(o));}catch{}return r.size>=A&&r.clear(),r.set(t,n),n}var k=e=>e.replace(/^webpack:\/\/[^/]*\//,"").replace(/^\.\//,"");async function g(e,t){let r=new Set;for(let o of e.match(/\S+/g)??[]){let s=m(o);s&&r.add(s.url);}let n=new Map;for(let o of r)n.set(o,await R(t,o));return e.replace(/\S+/g,o=>{let s=m(o),a=s&&n.get(s.url)?.originalPositionFor({line:s.line,column:s.column-1});return !s||!a?.source||!a.line?o:`${s.before}${k(a.source)}:${a.line}:${a.column+1}${s.after}`})}function v(e){return async t=>{let r=new URL(t).pathname;if(!r.startsWith(e.urlPath))return;let{readFile:n}=await import('fs/promises'),o=await import('path'),s=o.resolve(e.dir),a=decodeURIComponent(r.slice(e.urlPath.length)).replace(/^\/+/,""),u=o.resolve(s,`${a}.map`);if(u.startsWith(s+o.sep))return JSON.parse(await n(u,"utf8"))}}var c=64*1024,O=200,x=5e3,i=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function P(e){let t;try{t=JSON.parse(e);}catch{return}if(!i(t)||!Array.isArray(t.resourceSpans))return;let r=0;for(let n of t.resourceSpans){if(!i(n)||!Array.isArray(n.scopeSpans)||n.resource!==void 0&&!i(n.resource))return;for(let o of n.scopeSpans){if(!i(o)||!Array.isArray(o.spans)||!o.spans.every(i))return;r+=o.spans.length;}}return r>0&&r<=O?t:void 0}function l(e){if(Array.isArray(e))for(let t of e){let r=t?.value;typeof r?.stringValue=="string"&&(r.stringValue=a(r.stringValue));}}function B(e){if(typeof e.name=="string"&&(e.name=a(e.name)),l(e.attributes),!!Array.isArray(e.events))for(let t of e.events)i(t)&&l(t.attributes);}async function _(e,t){if(Array.isArray(e.events)){for(let r of e.events)if(!(!i(r)||!Array.isArray(r.attributes)))for(let n of r.attributes){let o=n?.value;if(!(n?.key!=="exception.stacktrace"||typeof o?.stringValue!="string"))try{o.stringValue=await g(o.stringValue,t);}catch{}}}}function L(e,t){let r=new Set(Object.keys(t)),n=e.resource?.attributes,o=Array.isArray(n)?n.filter(a=>i(a)&&!r.has(String(a.key))):[],s=Object.entries(t).filter(([,a])=>a!==void 0).map(([a,u])=>({key:a,value:{stringValue:u}}));e.resource={...e.resource,attributes:[...o,...s]};}async function d(e,t){if(new TextEncoder().encode(e).length>c)return 413;let r=P(e);if(!r)return 400;for(let n of r.resourceSpans){l(n.resource?.attributes),L(n,t.trusted);for(let o of n.scopeSpans)for(let s of o.spans)B(s),t.sourceMaps&&await _(s,t.sourceMaps);}try{return (await fetch(t.endpoint,{method:"POST",headers:{...t.headers,"content-type":"application/json"},body:JSON.stringify(r),signal:AbortSignal.timeout(x)})).ok?204:502}catch{return 502}}var T=/\+https?:\/\/|Googlebot|bingbot|Applebot|crawler|spider|HeadlessChrome|Lighthouse|facebookexternalhit|WhatsApp\//i,N=[["instagram",/\bInstagram\b/],["facebook",/\bFB(AN|AV|_IAB)\b/],["snapchat",/\bSnapchat\b/],["linkedin",/\bLinkedInApp\b/],["android_webview",/; wv\)/]],b=e=>T.test(e);function S(e){return {"device.type":/iPad|Tablet/i.test(e)?"tablet":/Mobi|Android|iPhone|iPod/i.test(e)?"mobile":"desktop","browser.in_app":N.find(([,t])=>t.test(e))?.[0]}}var C=6e3;async function V(e,t){if(!e.body)return "";let r=e.body.getReader(),n=new TextDecoder,o="",s=0;for(;;){let{done:a,value:u}=await r.read();if(a)return o+n.decode();if(s+=u.byteLength,s>t){r.cancel();return}o+=n.decode(u,{stream:true});}}function E(e){let t=p(C);async function r(n){if(Number(n.headers.get("content-length"))>c)return 413;if(!t())return 429;let o=n.headers.get("user-agent")??"";if(b(o))return 204;try{let s=await V(n,c);return s===void 0?413:await d(s,{...e,trusted:{...S(o),...e.trusted}})}catch{return 400}}return async function(o){return new Response(null,{status:await r(o)})}}
2
+ export{c as MAX_BODY_BYTES,E as createRelayHandler,v as fileSourceMaps,d as processBatch};
@@ -0,0 +1,18 @@
1
+ import { Sampler, SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+
3
+ /**
4
+ * Parent-based, always-on sampler that never traces requests whose path matches `skip`
5
+ * (static assets, the telemetry relay): high volume, no diagnostic value.
6
+ */
7
+ declare function skipPathsSampler(skip: RegExp): Sampler;
8
+
9
+ /**
10
+ * Strips query strings, emails and phone numbers from span names, string attributes
11
+ * (frameworks put full URLs, with ad-click IDs and tokens, in both), status and events: an
12
+ * exception's message and stack can quote user input. Register it before
13
+ * the exporting processor. Mutates in onEnd: still before batch export, and some bundled
14
+ * SDKs never call onEnding.
15
+ */
16
+ declare function scrubSpanProcessor(): SpanProcessor;
17
+
18
+ export { scrubSpanProcessor, skipPathsSampler };
@@ -0,0 +1,18 @@
1
+ import { Sampler, SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+
3
+ /**
4
+ * Parent-based, always-on sampler that never traces requests whose path matches `skip`
5
+ * (static assets, the telemetry relay): high volume, no diagnostic value.
6
+ */
7
+ declare function skipPathsSampler(skip: RegExp): Sampler;
8
+
9
+ /**
10
+ * Strips query strings, emails and phone numbers from span names, string attributes
11
+ * (frameworks put full URLs, with ad-click IDs and tokens, in both), status and events: an
12
+ * exception's message and stack can quote user input. Register it before
13
+ * the exporting processor. Mutates in onEnd: still before batch export, and some bundled
14
+ * SDKs never call onEnding.
15
+ */
16
+ declare function scrubSpanProcessor(): SpanProcessor;
17
+
18
+ export { scrubSpanProcessor, skipPathsSampler };
@@ -0,0 +1,2 @@
1
+ 'use strict';var api=require('@opentelemetry/api');var c=0,p=2;function f(e){let t=e["url.path"]??e["http.target"];return typeof t=="string"?t.split("?")[0]:void 0}function g(e){return {shouldSample(t,s,n,R,m){let o=f(m);if(o&&e.test(o))return {decision:c};let i=api.trace.getSpanContext(t);return i?{decision:(i.traceFlags&api.TraceFlags.SAMPLED)===api.TraceFlags.SAMPLED?p:c}:{decision:p}},toString:()=>`SkipPathsSampler(${e})`}}var S=/\?[^\s#)'"]*?(?=(?::\d+:\d+)?(?:[\s#)'"]|$))/g,d=/(?<![\w.+-])[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g,b=/(?<!\w)(?:\+?91[\s-]?)?[6-9]\d{9}(?!\w)/g;function P(e){return e.replace(/\S+/g,t=>{let s=t.indexOf("#");if(s<0||!t.slice(0,s).includes("://"))return t;let n=t.slice(s).search(/[)'"]/);return t.slice(0,s)+(n<0?"":t.slice(s+n))})}function r(e){return P(e.replace(S,"")).replace(d,"[email]").replace(b,"[phone]")}function u(e){if(e)for(let[t,s]of Object.entries(e))typeof s=="string"&&(e[t]=r(s));}function h(){return {onStart(){},onEnd(e){e.name=r(e.name),u(e.attributes);for(let s of e.events)u(s.attributes);let t=e.status;t.message&&(t.message=r(t.message));},forceFlush:()=>Promise.resolve(),shutdown:()=>Promise.resolve()}}
2
+ exports.scrubSpanProcessor=h;exports.skipPathsSampler=g;
@@ -0,0 +1 @@
1
+ import {a}from'../chunk-ZNVBPNNQ.mjs';import {trace,TraceFlags}from'@opentelemetry/api';var i=0,p=2;function l(e){let t=e["url.path"]??e["http.target"];return typeof t=="string"?t.split("?")[0]:void 0}function f(e){return {shouldSample(t,r,d,g,c){let n=l(c);if(n&&e.test(n))return {decision:i};let o=trace.getSpanContext(t);return o?{decision:(o.traceFlags&TraceFlags.SAMPLED)===TraceFlags.SAMPLED?p:i}:{decision:p}},toString:()=>`SkipPathsSampler(${e})`}}function m(e){if(e)for(let[t,r]of Object.entries(e))typeof r=="string"&&(e[t]=a(r));}function S(){return {onStart(){},onEnd(e){e.name=a(e.name),m(e.attributes);for(let r of e.events)m(r.attributes);let t=e.status;t.message&&(t.message=a(t.message));},forceFlush:()=>Promise.resolve(),shutdown:()=>Promise.resolve()}}export{S as scrubSpanProcessor,f as skipPathsSampler};
package/package.json CHANGED
@@ -1,6 +1,100 @@
1
1
  {
2
2
  "name": "@shopkit/observability",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Frontend observability for storefronts and the editor: browser errors, failed requests, web vitals and flows, sent as OTLP traces",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "shopkit-move-sourcemaps": "./dist/bin/move-sourcemaps.js"
10
+ },
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ },
18
+ "./client": {
19
+ "types": "./dist/client/index.d.ts",
20
+ "import": "./dist/client/index.mjs",
21
+ "require": "./dist/client/index.js"
22
+ },
23
+ "./relay": {
24
+ "types": "./dist/relay/index.d.ts",
25
+ "import": "./dist/relay/index.mjs",
26
+ "require": "./dist/relay/index.js"
27
+ },
28
+ "./server": {
29
+ "types": "./dist/server/index.d.ts",
30
+ "import": "./dist/server/index.mjs",
31
+ "require": "./dist/server/index.js"
32
+ },
33
+ "./react": {
34
+ "types": "./dist/react/index.d.ts",
35
+ "import": "./dist/react/index.mjs",
36
+ "require": "./dist/react/index.js"
37
+ },
38
+ "./head-stub": {
39
+ "types": "./dist/head-stub/index.d.ts",
40
+ "import": "./dist/head-stub/index.mjs",
41
+ "require": "./dist/head-stub/index.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md"
47
+ ],
48
+ "scripts": {
49
+ "build": "rm -rf dist && tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "rm -rf dist",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest"
55
+ },
56
+ "devDependencies": {
57
+ "@opentelemetry/api": "^1.9.0",
58
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
59
+ "@types/node": "^20.8.10",
60
+ "@types/react": "^18.2.0",
61
+ "@types/react-dom": "^18.2.0",
62
+ "jsdom": "^23.0.0",
63
+ "react": "^18.2.0",
64
+ "react-dom": "^18.2.0",
65
+ "tsup": "^8.0.0",
66
+ "typescript": "^5.3.0",
67
+ "vitest": "^1.0.0"
68
+ },
69
+ "peerDependencies": {
70
+ "@opentelemetry/api": ">=1.9.0 <2.0.0",
71
+ "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0",
72
+ "react": ">=18.0.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@opentelemetry/api": {
76
+ "optional": true
77
+ },
78
+ "@opentelemetry/sdk-trace-base": {
79
+ "optional": true
80
+ },
81
+ "react": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "keywords": [
86
+ "observability",
87
+ "opentelemetry",
88
+ "otlp",
89
+ "monitoring",
90
+ "shopkit"
91
+ ],
92
+ "license": "MIT",
93
+ "publishConfig": {
94
+ "access": "public"
95
+ },
96
+ "dependencies": {
97
+ "source-map-js": "^1.2.1",
98
+ "web-vitals": "^6.2.3"
99
+ }
100
+ }