@snokam/application-insights 1.3.0 → 1.3.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.3.2](https://github.com/snokam/components/compare/%40snokam%2Fapplication-insights%401.3.1...%40snokam%2Fapplication-insights%401.3.2) (2026-11-10)
7
+
8
+ ### Bug Fixes
9
+
10
+ - error pipeline noise filters, burst alerts, telemetry roles and per-activity fan-out logs ([#3496](https://github.com/snokam/components/issues/3496)) ([e41e941](https://github.com/snokam/components/commit/e41e9415a899f075585923989c87b2ea4f27f273))
11
+
12
+ ## [1.3.1](https://github.com/snokam/components/compare/%40snokam%2Fapplication-insights%401.3.0...%40snokam%2Fapplication-insights%401.3.1) (2026-11-09)
13
+
14
+ ### Bug Fixes
15
+
16
+ - **deps:** close all open npm Dependabot alerts and bump deps within major ([#3364](https://github.com/snokam/components/issues/3364)) ([1372dbb](https://github.com/snokam/components/commit/1372dbbed19d6d6c93c357f3543f4c8db01bfb43))
17
+ - **observability:** always record 5xx and exceptions, report web server errors to App Insights ([#3298](https://github.com/snokam/components/issues/3298)) ([246229d](https://github.com/snokam/components/commit/246229d4774378c591155a145e4b9b5f64e0e708))
18
+ - **web:** never send the server token to an unknown host; dates in Europe/Oslo everywhere ([#3334](https://github.com/snokam/components/issues/3334)) ([a17581a](https://github.com/snokam/components/commit/a17581a457f91de68e646958509b490e39763b32)), closes [#3318](https://github.com/snokam/components/issues/3318) [#418](https://github.com/snokam/components/issues/418) [#3322](https://github.com/snokam/components/issues/3322) [#3328](https://github.com/snokam/components/issues/3328)
19
+
20
+ ### Performance Improvements
21
+
22
+ - **web:** cache public pages per host and speed up every app (Lighthouse round 2) ([#3279](https://github.com/snokam/components/issues/3279)) ([4ab508b](https://github.com/snokam/components/commit/4ab508ba90289a5d1bd28e7b843f9069dae5cc44))
23
+ - **web:** faster and more accessible public pages (Lighthouse) ([#3273](https://github.com/snokam/components/issues/3273)) ([0501fea](https://github.com/snokam/components/commit/0501feaeb803dec1c0a2993726a65f10a986f803)), closes [#1976d2](https://github.com/snokam/components/issues/1976d2) [#1565c0](https://github.com/snokam/components/issues/1565c0)
24
+ - **web:** halve the JavaScript on the signed-in apps ([#3281](https://github.com/snokam/components/issues/3281)) ([c248da6](https://github.com/snokam/components/commit/c248da646f6a9b457278042890b92976b07a8d16))
25
+
6
26
  # 1.3.0 (2026-08-18)
7
27
 
8
28
  ### Bug Fixes
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@snokam/application-insights",
3
3
  "description": "Snøkam core components",
4
- "version": "1.3.0",
4
+ "version": "1.3.2",
5
5
  "main": "src/index.tsx",
6
+ "sideEffects": false,
6
7
  "types": "src/index.tsx",
7
8
  "files": [
8
9
  "src",
@@ -18,18 +19,18 @@
18
19
  "access": "public"
19
20
  },
20
21
  "dependencies": {
21
- "@microsoft/applicationinsights-react-js": "^19.4.1",
22
- "@microsoft/applicationinsights-web": "^3.4.3",
22
+ "@microsoft/applicationinsights-react-js": "^19.4.2",
23
+ "@microsoft/applicationinsights-web": "^3.4.6",
23
24
  "history": "^5.3.0"
24
25
  },
25
26
  "peerDependencies": {
26
- "next": "16.2.11",
27
- "react": "19.2.8",
28
- "react-dom": "19.2.8"
27
+ "next": "16.4.5",
28
+ "react": "19.3.1",
29
+ "react-dom": "19.3.1"
29
30
  },
30
31
  "devDependencies": {
31
- "@types/react": "^19.2.17",
32
- "@types/react-dom": "^19.2.3"
32
+ "@types/react": "^19.3.0",
33
+ "@types/react-dom": "^19.3.0"
33
34
  },
34
35
  "exports": {
35
36
  ".": "./src/index.tsx",
@@ -38,6 +39,7 @@
38
39
  "./server": "./src/server.ts"
39
40
  },
40
41
  "scripts": {
41
- "lint": "eslint ."
42
+ "lint": "eslint .",
43
+ "test": "node --test \"src/**/*.test.ts\""
42
44
  }
43
45
  }
package/src/Provider.tsx CHANGED
@@ -11,10 +11,11 @@ import {
11
11
  IConfiguration,
12
12
  ITelemetryPlugin,
13
13
  } from "@microsoft/applicationinsights-web";
14
+ import { withCloudRole } from "./telemetry/cloudRole";
14
15
 
15
16
  export interface ProviderProps {
16
17
  children: ReactNode;
17
- config: IConfiguration & IConfig;
18
+ config: IConfiguration & IConfig & { cloudRole?: string };
18
19
  }
19
20
 
20
21
  export const Provider = ({ children, config }: ProviderProps) => {
@@ -28,6 +29,7 @@ export const Provider = ({ children, config }: ProviderProps) => {
28
29
 
29
30
  useEffect(() => {
30
31
  if (!appInsightsRef.current) {
32
+ const { cloudRole, ...sdkConfig } = config;
31
33
  appInsightsRef.current = new ApplicationInsights({
32
34
  config: {
33
35
  maxBatchSizeInBytes: 10000,
@@ -37,11 +39,12 @@ export const Provider = ({ children, config }: ProviderProps) => {
37
39
  extensionConfig: {
38
40
  [reactPluginRef.current.identifier]: { history: browserHistory },
39
41
  },
40
- ...config,
42
+ ...sdkConfig,
41
43
  },
42
44
  });
43
45
 
44
46
  appInsightsRef.current.loadAppInsights();
47
+ appInsightsRef.current.addTelemetryInitializer(withCloudRole(cloudRole));
45
48
 
46
49
  appInsightsRef.current.addTelemetryInitializer((envelope) => {
47
50
  const baseType = envelope.baseType || "";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { usePathname, useSearchParams } from "next/navigation";
4
- import { ReactNode, Suspense, useEffect, useRef } from "react";
4
+ import { ReactNode, Suspense, useEffect, useRef, useState } from "react";
5
5
  import { createBrowserHistory } from "history";
6
6
  import {
7
7
  AppInsightsContext,
@@ -13,75 +13,79 @@ import {
13
13
  IConfiguration,
14
14
  ITelemetryPlugin,
15
15
  } from "@microsoft/applicationinsights-web";
16
+ import { withCloudRole } from "./telemetry/cloudRole";
16
17
 
17
18
  export interface ProviderProps {
18
19
  children: ReactNode;
19
- config: IConfiguration & IConfig;
20
+ config: IConfiguration & IConfig & { cloudRole?: string };
20
21
  }
21
22
 
22
- function AppRouterProvider({ children, config }: ProviderProps) {
23
- return (
24
- <Suspense fallback={<>{children}</>}>
25
- <AppRouterProviderInner config={config}>
26
- {children}
27
- </AppRouterProviderInner>
28
- </Suspense>
29
- );
30
- }
31
-
32
- function AppRouterProviderInner({ children, config }: ProviderProps) {
23
+ function PageViewTracker({
24
+ instance,
25
+ }: {
26
+ instance: ApplicationInsights | null;
27
+ }) {
33
28
  const pathname = usePathname();
34
29
  const searchParams = useSearchParams();
35
- const reactPluginRef = useRef<ReactPlugin>(new ReactPlugin());
36
- const appInsightsRef = useRef<ApplicationInsights | null>(null);
37
30
 
38
- const isBrowser = typeof window !== "undefined";
39
- const browserHistory = isBrowser ? createBrowserHistory() : undefined;
31
+ useEffect(() => {
32
+ if (!instance) return;
33
+ const url = pathname + (searchParams?.toString() ? `?${searchParams}` : "");
34
+ instance.trackPageView({ name: url });
35
+ }, [instance, pathname, searchParams]);
36
+
37
+ return null;
38
+ }
39
+
40
+ function AppRouterProvider({ children, config }: ProviderProps) {
41
+ const reactPluginRef = useRef<ReactPlugin>(new ReactPlugin());
42
+ const [instance, setInstance] = useState<ApplicationInsights | null>(null);
43
+ const started = useRef(false);
40
44
 
41
45
  useEffect(() => {
42
- if (!appInsightsRef.current) {
43
- appInsightsRef.current = new ApplicationInsights({
44
- config: {
45
- maxBatchSizeInBytes: 10000,
46
- maxBatchInterval: 15000,
47
- enableAutoRouteTracking: true,
48
- extensions: [reactPluginRef.current as unknown as ITelemetryPlugin],
49
- extensionConfig: {
50
- [reactPluginRef.current.identifier]: { history: browserHistory },
51
- },
52
- ...config,
46
+ if (started.current) return;
47
+ started.current = true;
48
+ const browserHistory = createBrowserHistory();
49
+ const { cloudRole, ...sdkConfig } = config;
50
+ const appInsights = new ApplicationInsights({
51
+ config: {
52
+ maxBatchSizeInBytes: 10000,
53
+ maxBatchInterval: 15000,
54
+ enableAutoRouteTracking: true,
55
+ disablePageUnloadEvents: ["unload"],
56
+ extensions: [reactPluginRef.current as unknown as ITelemetryPlugin],
57
+ extensionConfig: {
58
+ [reactPluginRef.current.identifier]: { history: browserHistory },
53
59
  },
54
- });
60
+ ...sdkConfig,
61
+ },
62
+ });
55
63
 
56
- appInsightsRef.current.loadAppInsights();
64
+ appInsights.loadAppInsights();
65
+ appInsights.addTelemetryInitializer(withCloudRole(cloudRole));
57
66
 
58
- appInsightsRef.current.addTelemetryInitializer((envelope) => {
59
- const baseType = envelope.baseType || "";
60
- const telemetryItem = envelope.baseData || {};
61
- const target = telemetryItem.target || "";
62
- const isDependencyData = baseType === "RemoteDependencyData";
67
+ appInsights.addTelemetryInitializer((envelope) => {
68
+ const baseType = envelope.baseType || "";
69
+ const telemetryItem = envelope.baseData || {};
70
+ const target = telemetryItem.target || "";
71
+ const isDependencyData = baseType === "RemoteDependencyData";
63
72
 
64
- const isVideo =
65
- target.includes("mux.com") || target.includes("litix.io");
73
+ const isVideo = target.includes("mux.com") || target.includes("litix.io");
66
74
 
67
- if (isDependencyData && isVideo) {
68
- return false;
69
- }
70
- });
71
- }
72
- }, [config, browserHistory]);
75
+ if (isDependencyData && isVideo) {
76
+ return false;
77
+ }
78
+ });
73
79
 
74
- useEffect(() => {
75
- if (appInsightsRef.current) {
76
- const url =
77
- pathname + (searchParams?.toString() ? `?${searchParams}` : "");
78
- appInsightsRef.current.trackPageView({ name: url });
79
- }
80
- }, [pathname, searchParams]);
80
+ setInstance(appInsights);
81
+ }, [config]);
81
82
 
82
83
  return (
83
84
  <AppInsightsContext.Provider value={reactPluginRef.current}>
84
85
  {children}
86
+ <Suspense fallback={null}>
87
+ <PageViewTracker instance={instance} />
88
+ </Suspense>
85
89
  </AppInsightsContext.Provider>
86
90
  );
87
91
  }
package/src/server.ts CHANGED
@@ -1,3 +1,11 @@
1
+ import { parseConnectionString } from "./telemetry/connectionString";
2
+ import {
3
+ exceptionEnvelope,
4
+ type RequestLocation,
5
+ type ServerError,
6
+ } from "./telemetry/exceptionEnvelope";
7
+ import { operationIdFrom } from "./telemetry/operationId";
8
+
1
9
  type ErrorRequest = {
2
10
  path?: string;
3
11
  method?: string;
@@ -12,22 +20,55 @@ type ErrorContext = {
12
20
  revalidateReason?: string;
13
21
  };
14
22
 
15
- export function onRequestError(
23
+ async function trackException(serverError: ServerError) {
24
+ const ingestion = parseConnectionString(
25
+ process.env.APPLICATIONINSIGHTS_CONNECTION_STRING
26
+ );
27
+ const site = process.env.WEBSITE_SITE_NAME;
28
+ if (!ingestion || !site) return;
29
+
30
+ const envelope = exceptionEnvelope(
31
+ ingestion.instrumentationKey,
32
+ serverError,
33
+ { name: site, instance: process.env.HOSTNAME },
34
+ new Date()
35
+ );
36
+ try {
37
+ const response = await fetch(ingestion.trackUrl, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify([envelope]),
41
+ });
42
+ if (!response.ok)
43
+ console.error("[server-error] telemetry rejected", response.status);
44
+ } catch (failure) {
45
+ console.error("[server-error] telemetry unreachable", failure);
46
+ }
47
+ }
48
+
49
+ export async function onRequestError(
16
50
  error: unknown,
17
51
  request: ErrorRequest,
18
52
  context: ErrorContext
19
53
  ) {
20
- const host =
21
- request.headers?.["x-forwarded-host"] ?? request.headers?.["host"];
22
- const detail = {
23
- host,
54
+ const location: RequestLocation = {
55
+ host: request.headers?.["x-forwarded-host"] ?? request.headers?.["host"],
24
56
  method: request.method,
25
57
  path: request.path,
26
58
  routePath: context.routePath,
27
59
  routeType: context.routeType,
28
60
  renderSource: context.renderSource,
29
- message: error instanceof Error ? error.message : String(error),
30
- stack: error instanceof Error ? error.stack : undefined,
61
+ operationId: operationIdFrom(request.headers?.["traceparent"], () =>
62
+ crypto.randomUUID().replaceAll("-", "")
63
+ ),
31
64
  };
32
- console.error("[server-error]", JSON.stringify(detail));
65
+ console.error(
66
+ "[server-error]",
67
+ JSON.stringify({
68
+ ...location,
69
+ message: error instanceof Error ? error.message : String(error),
70
+ stack: error instanceof Error ? error.stack : undefined,
71
+ })
72
+ );
73
+ await trackException({ error, location });
33
74
  }
@@ -0,0 +1,8 @@
1
+ import type { ITelemetryItem } from "@microsoft/applicationinsights-web";
2
+
3
+ export function withCloudRole(cloudRole: string | undefined) {
4
+ return (envelope: ITelemetryItem) => {
5
+ if (!cloudRole) return;
6
+ envelope.tags = { ...envelope.tags, "ai.cloud.role": cloudRole };
7
+ };
8
+ }
@@ -0,0 +1,33 @@
1
+ export type Ingestion = {
2
+ instrumentationKey: string;
3
+ trackUrl: string;
4
+ };
5
+
6
+ const TRACK_PATH = "v2.1/track";
7
+
8
+ function settings(connectionString: string) {
9
+ const entries = connectionString.split(";").map((part): [string, string] => {
10
+ const separator = part.indexOf("=");
11
+ return [
12
+ part.slice(0, separator).trim().toLowerCase(),
13
+ part.slice(separator + 1).trim(),
14
+ ];
15
+ });
16
+ return new Map(entries.filter(([key]) => key.length > 0));
17
+ }
18
+
19
+ export function parseConnectionString(
20
+ connectionString: string | undefined
21
+ ): Ingestion | undefined {
22
+ if (!connectionString) return undefined;
23
+
24
+ const values = settings(connectionString);
25
+ const instrumentationKey = values.get("instrumentationkey");
26
+ const endpoint = values.get("ingestionendpoint");
27
+ if (!instrumentationKey || !endpoint) return undefined;
28
+
29
+ return {
30
+ instrumentationKey,
31
+ trackUrl: new URL(TRACK_PATH, endpoint).toString(),
32
+ };
33
+ }
@@ -0,0 +1,110 @@
1
+ export type RequestLocation = {
2
+ host?: string;
3
+ method?: string;
4
+ path?: string;
5
+ routePath?: string;
6
+ routeType?: string;
7
+ renderSource?: string;
8
+ operationId?: string;
9
+ };
10
+
11
+ export type ServerError = {
12
+ error: unknown;
13
+ location: RequestLocation;
14
+ };
15
+
16
+ export type Role = {
17
+ name?: string;
18
+ instance?: string;
19
+ };
20
+
21
+ const ERROR_SEVERITY = 3;
22
+
23
+ type ClientFailure = Error & {
24
+ digest?: string;
25
+ response?: { status?: number; url?: string };
26
+ cause?: unknown;
27
+ };
28
+
29
+ function withoutQuery(url: string | undefined) {
30
+ if (!url) return undefined;
31
+ try {
32
+ const parsed = new URL(url);
33
+ return `${parsed.origin}${parsed.pathname}`;
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
39
+ function causeOf(cause: unknown) {
40
+ if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
41
+ return cause === undefined ? undefined : String(cause);
42
+ }
43
+
44
+ function failedCall(error: ClientFailure) {
45
+ return {
46
+ status: error.response?.status?.toString(),
47
+ url: withoutQuery(error.response?.url),
48
+ cause: causeOf(error.cause),
49
+ };
50
+ }
51
+
52
+ function describe(error: unknown) {
53
+ if (error instanceof Error) {
54
+ const failure = error as ClientFailure;
55
+ return {
56
+ typeName: error.name,
57
+ message: error.message,
58
+ stack: error.stack,
59
+ details: { digest: failure.digest, ...failedCall(failure) },
60
+ };
61
+ }
62
+ return { typeName: "Error", message: String(error), details: {} };
63
+ }
64
+
65
+ function definedOnly(values: Record<string, string | undefined>) {
66
+ return Object.fromEntries(
67
+ Object.entries(values).filter(([, value]) => value !== undefined)
68
+ );
69
+ }
70
+
71
+ export function exceptionEnvelope(
72
+ instrumentationKey: string,
73
+ serverError: ServerError,
74
+ role: Role,
75
+ time: Date
76
+ ) {
77
+ const { typeName, message, stack, details } = describe(serverError.error);
78
+ const { location } = serverError;
79
+ const route = location.routePath ?? location.path;
80
+ const operationName = [location.method, route].filter(Boolean).join(" ");
81
+
82
+ return {
83
+ name: "Microsoft.ApplicationInsights.Exception",
84
+ time: time.toISOString(),
85
+ iKey: instrumentationKey,
86
+ tags: definedOnly({
87
+ "ai.cloud.role": role.name,
88
+ "ai.cloud.roleInstance": role.instance,
89
+ "ai.operation.name": operationName || undefined,
90
+ "ai.operation.id": location.operationId,
91
+ }),
92
+ data: {
93
+ baseType: "ExceptionData",
94
+ baseData: {
95
+ ver: 2,
96
+ severityLevel: ERROR_SEVERITY,
97
+ problemId: `${typeName} at ${route ?? "unknown route"}`,
98
+ exceptions: [
99
+ {
100
+ typeName,
101
+ message,
102
+ hasFullStack: stack !== undefined,
103
+ stack,
104
+ },
105
+ ],
106
+ properties: definedOnly({ ...location, ...details }),
107
+ },
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,10 @@
1
+ const TRACEPARENT = /^[\da-f]{2}-([\da-f]{32})-[\da-f]{16}-[\da-f]{2}$/;
2
+ const INVALID_TRACE_ID = "0".repeat(32);
3
+
4
+ export function operationIdFrom(
5
+ traceparent: string | undefined,
6
+ newId: () => string
7
+ ) {
8
+ const traceId = traceparent?.match(TRACEPARENT)?.[1];
9
+ return traceId && traceId !== INVALID_TRACE_ID ? traceId : newId();
10
+ }
@@ -0,0 +1,163 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseConnectionString } from "./connectionString.ts";
4
+ import { exceptionEnvelope } from "./exceptionEnvelope.ts";
5
+ import { operationIdFrom } from "./operationId.ts";
6
+ import { withCloudRole } from "./cloudRole.ts";
7
+
8
+ const connectionString =
9
+ "InstrumentationKey=7e4c6e13-0000-0000-0000-000000000000;IngestionEndpoint=https://norwayeast-0.in.applicationinsights.azure.com/;LiveEndpoint=https://norwayeast.livediagnostics.monitor.azure.com/;ApplicationId=abc";
10
+
11
+ test("the connection string gives the key and the track url", () => {
12
+ assert.deepEqual(parseConnectionString(connectionString), {
13
+ instrumentationKey: "7e4c6e13-0000-0000-0000-000000000000",
14
+ trackUrl:
15
+ "https://norwayeast-0.in.applicationinsights.azure.com/v2.1/track",
16
+ });
17
+ });
18
+
19
+ test("nothing is sent without a usable connection string", () => {
20
+ assert.equal(parseConnectionString(undefined), undefined);
21
+ assert.equal(parseConnectionString(""), undefined);
22
+ assert.equal(parseConnectionString("InstrumentationKey=abc"), undefined);
23
+ });
24
+
25
+ test("a render crash becomes an error-level exception for the site", () => {
26
+ const envelope = exceptionEnvelope(
27
+ "key",
28
+ {
29
+ error: new ReferenceError("localStorage is not defined"),
30
+ location: {
31
+ host: "snokam.test.snosky.no",
32
+ method: "GET",
33
+ path: "/skjer/julebord",
34
+ routePath: "/[host]/skjer/[slug]",
35
+ routeType: "render",
36
+ operationId: "4bf92f3577b34da6a3ce929d0e0e4736",
37
+ },
38
+ },
39
+ { name: "app-plfm-web-t", instance: "web-1" },
40
+ new Date("2026-11-08T10:00:00Z")
41
+ );
42
+
43
+ assert.equal(envelope.name, "Microsoft.ApplicationInsights.Exception");
44
+ assert.equal(envelope.iKey, "key");
45
+ assert.equal(envelope.time, "2026-11-08T10:00:00.000Z");
46
+ assert.equal(envelope.tags["ai.cloud.role"], "app-plfm-web-t");
47
+ assert.equal(envelope.tags["ai.operation.name"], "GET /[host]/skjer/[slug]");
48
+ assert.equal(
49
+ envelope.tags["ai.operation.id"],
50
+ "4bf92f3577b34da6a3ce929d0e0e4736"
51
+ );
52
+ const data = envelope.data.baseData;
53
+ assert.equal(data.severityLevel, 3);
54
+ assert.equal(data.problemId, "ReferenceError at /[host]/skjer/[slug]");
55
+ assert.equal(data.exceptions[0].typeName, "ReferenceError");
56
+ assert.equal(data.exceptions[0].message, "localStorage is not defined");
57
+ assert.equal(data.exceptions[0].hasFullStack, true);
58
+ assert.equal(data.properties.path, "/skjer/julebord");
59
+ assert.equal(data.properties.routePath, "/[host]/skjer/[slug]");
60
+ });
61
+
62
+ test("a thrown value that is not an Error is still reported", () => {
63
+ const envelope = exceptionEnvelope(
64
+ "key",
65
+ { error: "boom", location: { path: "/blogg/ny" } },
66
+ {},
67
+ new Date("2026-11-08T10:00:00Z")
68
+ );
69
+
70
+ const data = envelope.data.baseData;
71
+ assert.equal(data.exceptions[0].message, "boom");
72
+ assert.equal(data.exceptions[0].hasFullStack, false);
73
+ assert.equal(data.problemId, "Error at /blogg/ny");
74
+ assert.equal("ai.cloud.role" in envelope.tags, false);
75
+ });
76
+
77
+ test("a failed api call names the status and endpoint without its query", () => {
78
+ const failure = Object.assign(new Error("Response returned an error code"), {
79
+ name: "ResponseError",
80
+ response: {
81
+ status: 404,
82
+ url: "https://n8n.snosky.no/api/blog/v1.0/projects?email=a%40b.no",
83
+ },
84
+ });
85
+ const envelope = exceptionEnvelope(
86
+ "key",
87
+ { error: failure, location: { path: "/sider/sitemap.xml" } },
88
+ {},
89
+ new Date("2026-11-08T10:00:00Z")
90
+ );
91
+
92
+ const { properties } = envelope.data.baseData;
93
+ assert.equal(properties.status, "404");
94
+ assert.equal(properties.url, "https://n8n.snosky.no/api/blog/v1.0/projects");
95
+ assert.equal("cause" in properties, false);
96
+ });
97
+
98
+ test("a request that never got a response names its cause", () => {
99
+ const failure = Object.assign(new Error("The request failed"), {
100
+ name: "FetchError",
101
+ cause: new DOMException(
102
+ "The operation was aborted due to timeout",
103
+ "TimeoutError"
104
+ ),
105
+ });
106
+ const envelope = exceptionEnvelope(
107
+ "key",
108
+ { error: failure, location: { path: "/" } },
109
+ {},
110
+ new Date("2026-11-08T10:00:00Z")
111
+ );
112
+
113
+ assert.equal(
114
+ envelope.data.baseData.properties.cause,
115
+ "TimeoutError: The operation was aborted due to timeout"
116
+ );
117
+ });
118
+
119
+ test("a server error joins the trace of the request that caused it", () => {
120
+ const id = operationIdFrom(
121
+ "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
122
+ () => "fresh"
123
+ );
124
+
125
+ assert.equal(id, "4bf92f3577b34da6a3ce929d0e0e4736");
126
+ });
127
+
128
+ test("a server error without a usable trace gets its own operation id", () => {
129
+ assert.equal(
130
+ operationIdFrom(undefined, () => "fresh"),
131
+ "fresh"
132
+ );
133
+ assert.equal(
134
+ operationIdFrom("garbage", () => "fresh"),
135
+ "fresh"
136
+ );
137
+ assert.equal(
138
+ operationIdFrom(
139
+ "00-00000000000000000000000000000000-00f067aa0ba902b7-01",
140
+ () => "fresh"
141
+ ),
142
+ "fresh"
143
+ );
144
+ });
145
+
146
+ test("browser telemetry carries the configured role", () => {
147
+ const envelope = { name: "PageView", tags: { "ai.session.id": "s" } };
148
+
149
+ withCloudRole("web-browser")(envelope);
150
+
151
+ assert.deepEqual(envelope.tags, {
152
+ "ai.session.id": "s",
153
+ "ai.cloud.role": "web-browser",
154
+ });
155
+ });
156
+
157
+ test("browser telemetry is left alone without a role", () => {
158
+ const envelope = { name: "PageView", tags: {} };
159
+
160
+ withCloudRole(undefined)(envelope);
161
+
162
+ assert.deepEqual(envelope.tags, {});
163
+ });