@dash0/sdk-web 0.18.4 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dash0.iife.js +1 -1
- package/dist/dash0.iife.js.map +1 -1
- package/dist/dash0.js +1 -1
- package/dist/dash0.js.map +1 -1
- package/dist/dash0.umd.cjs +1 -1
- package/dist/dash0.umd.cjs.map +1 -1
- package/dist/modules/api/init.js +11 -3
- package/dist/modules/api/init_test.js +74 -1
- package/dist/modules/semantic-conventions.js +1 -0
- package/dist/modules/utils/index.js +1 -0
- package/dist/modules/utils/sanitize.js +12 -0
- package/dist/modules/utils/sanitize_test.js +52 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/semantic-conventions.d.ts +1 -0
- package/dist/types/types/options.d.ts +11 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/sanitize.d.ts +9 -0
- package/dist/types/utils/sanitize_test.d.ts +1 -0
- package/package.json +1 -1
- package/src/api/init.ts +11 -1
- package/src/api/init_test.ts +95 -1
- package/src/semantic-conventions.ts +1 -0
- package/src/types/options.ts +12 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/sanitize.ts +13 -0
- package/src/utils/sanitize_test.ts +60 -0
package/dist/modules/api/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { vars } from "../vars";
|
|
2
|
-
import { DEPLOYMENT_ENVIRONMENT_NAME, DEPLOYMENT_ID, DEPLOYMENT_NAME, PAGE_LOAD_ID, SERVICE_NAME, SERVICE_VERSION, USER_AGENT, } from "../semantic-conventions";
|
|
3
|
-
import { fetch, generateUniqueId, PAGE_LOAD_ID_BYTES, warn, debug, perf, nav, win, NO_VALUE_FALLBACK, pick, loc, } from "../utils";
|
|
2
|
+
import { DEPLOYMENT_ENVIRONMENT_NAME, DEPLOYMENT_ID, DEPLOYMENT_NAME, PAGE_LOAD_ID, SERVICE_NAME, SERVICE_NAMESPACE, SERVICE_VERSION, USER_AGENT, } from "../semantic-conventions";
|
|
3
|
+
import { fetch, generateUniqueId, isSafeServiceName, PAGE_LOAD_ID_BYTES, warn, debug, perf, nav, win, NO_VALUE_FALLBACK, pick, loc, } from "../utils";
|
|
4
4
|
import { trackSessions } from "./session";
|
|
5
5
|
import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
|
|
6
6
|
import { startErrorInstrumentation } from "../instrumentations/errors";
|
|
@@ -23,10 +23,15 @@ export function init(opts) {
|
|
|
23
23
|
debug("Stopping Dash0 Web SDK initialization. This browser does not support the necessary APIs.");
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const trimmedServiceName = opts.serviceName.trim();
|
|
27
|
+
if (!trimmedServiceName) {
|
|
27
28
|
debug("Missing or empty serviceName value. Falling back to location.hostname.");
|
|
28
29
|
opts.serviceName = loc?.hostname ?? "unknown";
|
|
29
30
|
}
|
|
31
|
+
else if (opts.rejectSuspiciousServiceName !== false && !isSafeServiceName(trimmedServiceName)) {
|
|
32
|
+
debug("serviceName contains disallowed characters. Falling back to location.hostname.");
|
|
33
|
+
opts.serviceName = loc?.hostname ?? "unknown";
|
|
34
|
+
}
|
|
30
35
|
vars.endpoints = opts.endpoint instanceof Array ? opts.endpoint : [opts.endpoint];
|
|
31
36
|
if (vars.endpoints.length === 0) {
|
|
32
37
|
warn("No telemetry endpoint configured. Aborting Dash0 Web SDK initialization process.");
|
|
@@ -66,6 +71,9 @@ export function init(opts) {
|
|
|
66
71
|
}
|
|
67
72
|
function initializeResourceAttributes(opts) {
|
|
68
73
|
addAttribute(vars.resource.attributes, SERVICE_NAME, opts["serviceName"]);
|
|
74
|
+
if (opts.serviceNamespace) {
|
|
75
|
+
addAttribute(vars.resource.attributes, SERVICE_NAMESPACE, opts["serviceNamespace"]);
|
|
76
|
+
}
|
|
69
77
|
if (opts.serviceVersion) {
|
|
70
78
|
addAttribute(vars.resource.attributes, SERVICE_VERSION, opts["serviceVersion"]);
|
|
71
79
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { SERVICE_NAME } from "../semantic-conventions";
|
|
2
|
+
import { SERVICE_NAME, SERVICE_NAMESPACE } from "../semantic-conventions";
|
|
3
3
|
// Mock all the instrumentation modules
|
|
4
4
|
vi.mock("../instrumentations/web-vitals", () => ({
|
|
5
5
|
startWebVitalsInstrumentation: vi.fn(),
|
|
@@ -188,6 +188,21 @@ describe("init", () => {
|
|
|
188
188
|
expect(spyOnWarn).not.toHaveBeenCalled();
|
|
189
189
|
});
|
|
190
190
|
});
|
|
191
|
+
describe("serviceNamespace", () => {
|
|
192
|
+
it("should add serviceNamespace to resource attributes when provided", async () => {
|
|
193
|
+
init({
|
|
194
|
+
...baseOptions,
|
|
195
|
+
serviceNamespace: "my-namespace",
|
|
196
|
+
});
|
|
197
|
+
const namespaceAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAMESPACE);
|
|
198
|
+
expect(namespaceAttr?.value.stringValue).toBe("my-namespace");
|
|
199
|
+
});
|
|
200
|
+
it("should not add serviceNamespace to resource attributes when not provided", async () => {
|
|
201
|
+
init(baseOptions);
|
|
202
|
+
const namespaceAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAMESPACE);
|
|
203
|
+
expect(namespaceAttr).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
191
206
|
describe("serviceName fallback", () => {
|
|
192
207
|
it("should use provided serviceName when it is valid", async () => {
|
|
193
208
|
init({
|
|
@@ -221,6 +236,64 @@ describe("init", () => {
|
|
|
221
236
|
const serviceNameAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAME);
|
|
222
237
|
expect(serviceNameAttr?.value.stringValue).toBe("test-hostname.example.com");
|
|
223
238
|
});
|
|
239
|
+
it.each([
|
|
240
|
+
["single quote", "evil';DROP TABLE users;--"],
|
|
241
|
+
["double quote", 'svc"name'],
|
|
242
|
+
["semicolon", "svc;injected"],
|
|
243
|
+
["open brace", "svc${tpl}"],
|
|
244
|
+
["close brace", "svc}name"],
|
|
245
|
+
["less-than", "<script>"],
|
|
246
|
+
["greater-than", "svc>name"],
|
|
247
|
+
["embedded newline", "svc\nname"],
|
|
248
|
+
["NUL byte", "svc\x00name"],
|
|
249
|
+
])("should fallback to location.hostname by default when serviceName contains %s", async (_label, suspicious) => {
|
|
250
|
+
init({
|
|
251
|
+
...baseOptions,
|
|
252
|
+
serviceName: suspicious,
|
|
253
|
+
});
|
|
254
|
+
const serviceNameAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAME);
|
|
255
|
+
expect(serviceNameAttr?.value.stringValue).toBe("test-hostname.example.com");
|
|
256
|
+
});
|
|
257
|
+
it.each([
|
|
258
|
+
["single quote", "evil';DROP TABLE users;--"],
|
|
259
|
+
["double quote", 'svc"name'],
|
|
260
|
+
["semicolon", "svc;injected"],
|
|
261
|
+
["embedded newline", "svc\nname"],
|
|
262
|
+
])("should also fallback when rejectSuspiciousServiceName is explicitly true and serviceName contains %s", async (_label, suspicious) => {
|
|
263
|
+
init({
|
|
264
|
+
...baseOptions,
|
|
265
|
+
serviceName: suspicious,
|
|
266
|
+
rejectSuspiciousServiceName: true,
|
|
267
|
+
});
|
|
268
|
+
const serviceNameAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAME);
|
|
269
|
+
expect(serviceNameAttr?.value.stringValue).toBe("test-hostname.example.com");
|
|
270
|
+
});
|
|
271
|
+
it.each([
|
|
272
|
+
["single quote", "evil';DROP TABLE users;--"],
|
|
273
|
+
["semicolon", "svc;injected"],
|
|
274
|
+
["less-than", "<script>"],
|
|
275
|
+
])("should keep suspicious serviceName unchanged when rejectSuspiciousServiceName is explicitly false (%s)", async (_label, suspicious) => {
|
|
276
|
+
init({
|
|
277
|
+
...baseOptions,
|
|
278
|
+
serviceName: suspicious,
|
|
279
|
+
rejectSuspiciousServiceName: false,
|
|
280
|
+
});
|
|
281
|
+
const serviceNameAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAME);
|
|
282
|
+
expect(serviceNameAttr?.value.stringValue).toBe(suspicious);
|
|
283
|
+
});
|
|
284
|
+
it.each([
|
|
285
|
+
["forward slash", "myteam/myservice"],
|
|
286
|
+
["backslash", "myteam\\myservice"],
|
|
287
|
+
["spaces", "my service"],
|
|
288
|
+
["dots and hyphens", "svc-name.v2"],
|
|
289
|
+
])("should keep serviceName with allowed characters like %s under the default configuration", async (_label, allowed) => {
|
|
290
|
+
init({
|
|
291
|
+
...baseOptions,
|
|
292
|
+
serviceName: allowed,
|
|
293
|
+
});
|
|
294
|
+
const serviceNameAttr = vars.resource.attributes.find((attr) => attr.key === SERVICE_NAME);
|
|
295
|
+
expect(serviceNameAttr?.value.stringValue).toBe(allowed);
|
|
296
|
+
});
|
|
224
297
|
it("should fallback to 'unknown' when serviceName is empty and location.hostname is not available", async () => {
|
|
225
298
|
vi.resetModules();
|
|
226
299
|
// Mock utils module with loc as undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Resource Attribute Keys
|
|
2
2
|
export const SERVICE_NAME = "service.name";
|
|
3
|
+
export const SERVICE_NAMESPACE = "service.namespace";
|
|
3
4
|
export const SERVICE_VERSION = "service.version";
|
|
4
5
|
export const DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
|
|
5
6
|
export const DEPLOYMENT_NAME = "deployment.name";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const SUSPICIOUS_CHARS = /['"<>{};\x00-\x1F\x7F]/;
|
|
2
|
+
/**
|
|
3
|
+
* Returns true if the value is safe to use as a `service.name` resource attribute.
|
|
4
|
+
*
|
|
5
|
+
* Rejects values containing characters commonly used in injection payloads:
|
|
6
|
+
* quotes, angle brackets, braces, semicolons, and C0 control characters / DEL.
|
|
7
|
+
* Forward and back slashes are intentionally permitted so names like
|
|
8
|
+
* `myteam/myservice` remain valid.
|
|
9
|
+
*/
|
|
10
|
+
export function isSafeServiceName(value) {
|
|
11
|
+
return !SUSPICIOUS_CHARS.test(value);
|
|
12
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isSafeServiceName } from "./sanitize";
|
|
3
|
+
describe("isSafeServiceName", () => {
|
|
4
|
+
describe("accepts safe values", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
"my-service",
|
|
7
|
+
"my_service",
|
|
8
|
+
"my.service",
|
|
9
|
+
"MyService123",
|
|
10
|
+
"myteam/myservice",
|
|
11
|
+
"myteam\\myservice",
|
|
12
|
+
"service with spaces",
|
|
13
|
+
"service-name.v2",
|
|
14
|
+
"résumé-service",
|
|
15
|
+
"サービス",
|
|
16
|
+
"a",
|
|
17
|
+
])("accepts %j", (value) => {
|
|
18
|
+
expect(isSafeServiceName(value)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it("accepts the empty string (empty/whitespace handling lives elsewhere)", () => {
|
|
21
|
+
expect(isSafeServiceName("")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("rejects suspicious characters", () => {
|
|
25
|
+
it.each([
|
|
26
|
+
["single quote", "evil';DROP TABLE"],
|
|
27
|
+
["double quote", 'say "hi"'],
|
|
28
|
+
["semicolon", "a;b"],
|
|
29
|
+
["open brace", "${injected}"],
|
|
30
|
+
["close brace", "trailing}"],
|
|
31
|
+
["less-than", "<script>"],
|
|
32
|
+
["greater-than", "value>other"],
|
|
33
|
+
])("rejects %s", (_label, value) => {
|
|
34
|
+
expect(isSafeServiceName(value)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it("rejects NUL", () => {
|
|
37
|
+
expect(isSafeServiceName("a\x00b")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it("rejects embedded newline (log injection)", () => {
|
|
40
|
+
expect(isSafeServiceName("line1\nline2")).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it("rejects embedded carriage return", () => {
|
|
43
|
+
expect(isSafeServiceName("a\rb")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
it("rejects embedded tab", () => {
|
|
46
|
+
expect(isSafeServiceName("a\tb")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it("rejects DEL", () => {
|
|
49
|
+
expect(isSafeServiceName("a\x7Fb")).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|