@snokam/application-insights 1.3.1 → 1.3.3
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 +13 -0
- package/package.json +1 -1
- package/src/Provider.tsx +5 -2
- package/src/app-router.tsx +5 -2
- package/src/server.ts +10 -2
- package/src/telemetry/cloudRole.ts +8 -0
- package/src/telemetry/exceptionEnvelope.ts +2 -0
- package/src/telemetry/operationId.ts +10 -0
- package/src/telemetry/requestNoise.test.ts +73 -0
- package/src/telemetry/requestNoise.ts +27 -0
- package/src/telemetry/telemetry.test.ts +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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.3](https://github.com/snokam/components/compare/%40snokam%2Fapplication-insights%401.3.1...%40snokam%2Fapplication-insights%401.3.3) (2026-11-11)
|
|
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
|
+
- **web:** prod error cluster: story swiper crash, scanner noise, stale-deployment reload ([#3530](https://github.com/snokam/components/issues/3530)) ([f3a6c67](https://github.com/snokam/components/commit/f3a6c6713acfda5e087c4ed74881ecb95f0b76b1))
|
|
12
|
+
|
|
13
|
+
## [1.3.2](https://github.com/snokam/components/compare/%40snokam%2Fapplication-insights%401.3.1...%40snokam%2Fapplication-insights%401.3.2) (2026-11-10)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- 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))
|
|
18
|
+
|
|
6
19
|
## [1.3.1](https://github.com/snokam/components/compare/%40snokam%2Fapplication-insights%401.3.0...%40snokam%2Fapplication-insights%401.3.1) (2026-11-09)
|
|
7
20
|
|
|
8
21
|
### Bug Fixes
|
package/package.json
CHANGED
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
|
-
...
|
|
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 || "";
|
package/src/app-router.tsx
CHANGED
|
@@ -13,10 +13,11 @@ 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
23
|
function PageViewTracker({
|
|
@@ -45,6 +46,7 @@ function AppRouterProvider({ children, config }: ProviderProps) {
|
|
|
45
46
|
if (started.current) return;
|
|
46
47
|
started.current = true;
|
|
47
48
|
const browserHistory = createBrowserHistory();
|
|
49
|
+
const { cloudRole, ...sdkConfig } = config;
|
|
48
50
|
const appInsights = new ApplicationInsights({
|
|
49
51
|
config: {
|
|
50
52
|
maxBatchSizeInBytes: 10000,
|
|
@@ -55,11 +57,12 @@ function AppRouterProvider({ children, config }: ProviderProps) {
|
|
|
55
57
|
extensionConfig: {
|
|
56
58
|
[reactPluginRef.current.identifier]: { history: browserHistory },
|
|
57
59
|
},
|
|
58
|
-
...
|
|
60
|
+
...sdkConfig,
|
|
59
61
|
},
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
appInsights.loadAppInsights();
|
|
65
|
+
appInsights.addTelemetryInitializer(withCloudRole(cloudRole));
|
|
63
66
|
|
|
64
67
|
appInsights.addTelemetryInitializer((envelope) => {
|
|
65
68
|
const baseType = envelope.baseType || "";
|
package/src/server.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
type RequestLocation,
|
|
5
5
|
type ServerError,
|
|
6
6
|
} from "./telemetry/exceptionEnvelope";
|
|
7
|
+
import { operationIdFrom } from "./telemetry/operationId";
|
|
8
|
+
import { isRequestNoise } from "./telemetry/requestNoise";
|
|
7
9
|
|
|
8
10
|
type ErrorRequest = {
|
|
9
11
|
path?: string;
|
|
@@ -57,14 +59,20 @@ export async function onRequestError(
|
|
|
57
59
|
routePath: context.routePath,
|
|
58
60
|
routeType: context.routeType,
|
|
59
61
|
renderSource: context.renderSource,
|
|
62
|
+
operationId: operationIdFrom(request.headers?.["traceparent"], () =>
|
|
63
|
+
crypto.randomUUID().replaceAll("-", "")
|
|
64
|
+
),
|
|
60
65
|
};
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
const noise = isRequestNoise(error, location);
|
|
67
|
+
const log = noise ? console.warn : console.error;
|
|
68
|
+
log(
|
|
69
|
+
noise ? "[server-request-noise]" : "[server-error]",
|
|
63
70
|
JSON.stringify({
|
|
64
71
|
...location,
|
|
65
72
|
message: error instanceof Error ? error.message : String(error),
|
|
66
73
|
stack: error instanceof Error ? error.stack : undefined,
|
|
67
74
|
})
|
|
68
75
|
);
|
|
76
|
+
if (noise) return;
|
|
69
77
|
await trackException({ error, location });
|
|
70
78
|
}
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,7 @@ export type RequestLocation = {
|
|
|
5
5
|
routePath?: string;
|
|
6
6
|
routeType?: string;
|
|
7
7
|
renderSource?: string;
|
|
8
|
+
operationId?: string;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export type ServerError = {
|
|
@@ -86,6 +87,7 @@ export function exceptionEnvelope(
|
|
|
86
87
|
"ai.cloud.role": role.name,
|
|
87
88
|
"ai.cloud.roleInstance": role.instance,
|
|
88
89
|
"ai.operation.name": operationName || undefined,
|
|
90
|
+
"ai.operation.id": location.operationId,
|
|
89
91
|
}),
|
|
90
92
|
data: {
|
|
91
93
|
baseType: "ExceptionData",
|
|
@@ -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,73 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { isRequestNoise } from "./requestNoise.ts";
|
|
4
|
+
|
|
5
|
+
const page = {
|
|
6
|
+
method: "POST",
|
|
7
|
+
path: "/",
|
|
8
|
+
routePath: "/(public)/www/[host]/page",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
test("a server action from another deployment or a scanner is not a server error", () => {
|
|
12
|
+
const error = new Error(
|
|
13
|
+
"Failed to find Server Action. This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
assert.equal(isRequestNoise(error, page), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a visitor who leaves before the page finished streaming is not a server error", () => {
|
|
20
|
+
const error = new Error("The destination stream closed early.");
|
|
21
|
+
|
|
22
|
+
assert.equal(
|
|
23
|
+
isRequestNoise(error, {
|
|
24
|
+
method: "GET",
|
|
25
|
+
path: "/?_rsc=abc",
|
|
26
|
+
routePath: "/minside/[host]",
|
|
27
|
+
}),
|
|
28
|
+
true
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a server action posted to a page that does not exist is not a server error", () => {
|
|
33
|
+
const error = Object.assign(
|
|
34
|
+
new Error(
|
|
35
|
+
"Invariant: Expected RSC response, got text/plain. This is a bug in Next.js."
|
|
36
|
+
),
|
|
37
|
+
{ name: "InvariantError" }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
assert.equal(
|
|
41
|
+
isRequestNoise(error, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: "/login",
|
|
44
|
+
routePath: "/_not-found/page",
|
|
45
|
+
}),
|
|
46
|
+
true
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the same invariant on a real page is still reported", () => {
|
|
51
|
+
const error = new Error(
|
|
52
|
+
"Invariant: Expected RSC response, got text/plain. This is a bug in Next.js."
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
assert.equal(isRequestNoise(error, page), false);
|
|
56
|
+
assert.equal(
|
|
57
|
+
isRequestNoise(error, { method: "GET", routePath: "/_not-found/page" }),
|
|
58
|
+
false
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("failed api calls and render crashes are still reported", () => {
|
|
63
|
+
const failed = Object.assign(new Error("Response returned an error code"), {
|
|
64
|
+
name: "ResponseError",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(isRequestNoise(failed, page), false);
|
|
68
|
+
assert.equal(
|
|
69
|
+
isRequestNoise(new ReferenceError("TRACK_BAR is not defined"), page),
|
|
70
|
+
false
|
|
71
|
+
);
|
|
72
|
+
assert.equal(isRequestNoise("boom", page), false);
|
|
73
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RequestLocation } from "./exceptionEnvelope";
|
|
2
|
+
|
|
3
|
+
const UNKNOWN_SERVER_ACTION = "Failed to find Server Action";
|
|
4
|
+
const CLIENT_LEFT = "The destination stream closed early";
|
|
5
|
+
const ACTION_ON_MISSING_PAGE = "Expected RSC response, got text/plain";
|
|
6
|
+
const NOT_FOUND_ROUTE = "/_not-found/page";
|
|
7
|
+
|
|
8
|
+
function messageOf(error: unknown) {
|
|
9
|
+
return error instanceof Error ? error.message : String(error);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isActionOnMissingPage(message: string, location: RequestLocation) {
|
|
13
|
+
return (
|
|
14
|
+
message.includes(ACTION_ON_MISSING_PAGE) &&
|
|
15
|
+
location.method === "POST" &&
|
|
16
|
+
location.routePath === NOT_FOUND_ROUTE
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isRequestNoise(error: unknown, location: RequestLocation) {
|
|
21
|
+
const message = messageOf(error);
|
|
22
|
+
return (
|
|
23
|
+
message.startsWith(UNKNOWN_SERVER_ACTION) ||
|
|
24
|
+
message.startsWith(CLIENT_LEFT) ||
|
|
25
|
+
isActionOnMissingPage(message, location)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -2,6 +2,8 @@ import { test } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { parseConnectionString } from "./connectionString.ts";
|
|
4
4
|
import { exceptionEnvelope } from "./exceptionEnvelope.ts";
|
|
5
|
+
import { operationIdFrom } from "./operationId.ts";
|
|
6
|
+
import { withCloudRole } from "./cloudRole.ts";
|
|
5
7
|
|
|
6
8
|
const connectionString =
|
|
7
9
|
"InstrumentationKey=7e4c6e13-0000-0000-0000-000000000000;IngestionEndpoint=https://norwayeast-0.in.applicationinsights.azure.com/;LiveEndpoint=https://norwayeast.livediagnostics.monitor.azure.com/;ApplicationId=abc";
|
|
@@ -31,6 +33,7 @@ test("a render crash becomes an error-level exception for the site", () => {
|
|
|
31
33
|
path: "/skjer/julebord",
|
|
32
34
|
routePath: "/[host]/skjer/[slug]",
|
|
33
35
|
routeType: "render",
|
|
36
|
+
operationId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
34
37
|
},
|
|
35
38
|
},
|
|
36
39
|
{ name: "app-plfm-web-t", instance: "web-1" },
|
|
@@ -42,6 +45,10 @@ test("a render crash becomes an error-level exception for the site", () => {
|
|
|
42
45
|
assert.equal(envelope.time, "2026-11-08T10:00:00.000Z");
|
|
43
46
|
assert.equal(envelope.tags["ai.cloud.role"], "app-plfm-web-t");
|
|
44
47
|
assert.equal(envelope.tags["ai.operation.name"], "GET /[host]/skjer/[slug]");
|
|
48
|
+
assert.equal(
|
|
49
|
+
envelope.tags["ai.operation.id"],
|
|
50
|
+
"4bf92f3577b34da6a3ce929d0e0e4736"
|
|
51
|
+
);
|
|
45
52
|
const data = envelope.data.baseData;
|
|
46
53
|
assert.equal(data.severityLevel, 3);
|
|
47
54
|
assert.equal(data.problemId, "ReferenceError at /[host]/skjer/[slug]");
|
|
@@ -49,6 +56,7 @@ test("a render crash becomes an error-level exception for the site", () => {
|
|
|
49
56
|
assert.equal(data.exceptions[0].message, "localStorage is not defined");
|
|
50
57
|
assert.equal(data.exceptions[0].hasFullStack, true);
|
|
51
58
|
assert.equal(data.properties.path, "/skjer/julebord");
|
|
59
|
+
assert.equal(data.properties.routePath, "/[host]/skjer/[slug]");
|
|
52
60
|
});
|
|
53
61
|
|
|
54
62
|
test("a thrown value that is not an Error is still reported", () => {
|
|
@@ -107,3 +115,49 @@ test("a request that never got a response names its cause", () => {
|
|
|
107
115
|
"TimeoutError: The operation was aborted due to timeout"
|
|
108
116
|
);
|
|
109
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
|
+
});
|