@ogcio/o11y-sdk-node 0.3.1 → 0.4.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/lib/exporter/pii-exporter-decorator.d.ts +2 -0
  3. package/dist/lib/exporter/pii-exporter-decorator.js +27 -12
  4. package/dist/lib/index.d.ts +5 -0
  5. package/dist/lib/instrumentation.node.js +0 -8
  6. package/dist/lib/internals/redaction/pii-detection.d.ts +23 -0
  7. package/dist/lib/internals/redaction/pii-detection.js +87 -0
  8. package/dist/lib/internals/redaction/redactors/email.d.ts +8 -0
  9. package/dist/lib/internals/redaction/redactors/email.js +48 -0
  10. package/dist/lib/internals/redaction/redactors/index.d.ts +4 -0
  11. package/dist/lib/internals/redaction/redactors/index.js +6 -0
  12. package/dist/lib/internals/redaction/redactors/ip.d.ts +10 -0
  13. package/dist/lib/internals/redaction/redactors/ip.js +54 -0
  14. package/dist/lib/processor/enrich-logger-processor.d.ts +2 -2
  15. package/dist/package.json +14 -14
  16. package/dist/vitest.config.js +4 -4
  17. package/lib/exporter/pii-exporter-decorator.ts +48 -16
  18. package/lib/index.ts +5 -0
  19. package/lib/instrumentation.node.ts +0 -10
  20. package/lib/internals/redaction/pii-detection.ts +126 -0
  21. package/lib/internals/redaction/redactors/email.ts +58 -0
  22. package/lib/internals/redaction/redactors/index.ts +12 -0
  23. package/lib/internals/redaction/redactors/ip.ts +68 -0
  24. package/lib/internals/shared-metrics.ts +1 -1
  25. package/lib/processor/enrich-logger-processor.ts +2 -2
  26. package/package.json +14 -14
  27. package/test/internals/pii-detection.test.ts +27 -25
  28. package/test/internals/redactors/email.test.ts +81 -0
  29. package/test/internals/redactors/ip.test.ts +89 -0
  30. package/test/traces/active-span.test.ts +1 -1
  31. package/vitest.config.ts +4 -4
  32. package/dist/lib/internals/pii-detection.d.ts +0 -17
  33. package/dist/lib/internals/pii-detection.js +0 -116
  34. package/lib/internals/pii-detection.ts +0 -145
@@ -0,0 +1,126 @@
1
+ import type { AnyValue, AnyValueMap } from "@opentelemetry/api-logs";
2
+ import { Redactor } from "./redactors/index.js";
3
+ import { AttributeValue } from "@opentelemetry/api";
4
+
5
+ const decoder = new TextDecoder();
6
+ const encoder = new TextEncoder();
7
+
8
+ export type PIISource = "trace" | "log" | "metric";
9
+
10
+ /**
11
+ * Checks whether a string contains URI-encoded components.
12
+ *
13
+ * @param {string} value - The string to inspect.
14
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
15
+ */
16
+ function _containsEncodedComponents(value: string): boolean {
17
+ try {
18
+ return decodeURI(value) !== decodeURIComponent(value);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
26
+ *
27
+ * If the string is URL-encoded, it will be decoded before redaction.
28
+ * Metrics are emitted for:
29
+ * - each domain found in redacted email addresses.
30
+ * - IPv4|IPv6 addresses redacted.
31
+ *
32
+ * @template T
33
+ *
34
+ * @param {T} value - The input value to sanitize.
35
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
36
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
37
+ *
38
+ * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
39
+ */
40
+ export function _cleanStringPII<T extends AnyValue>(
41
+ value: T,
42
+ source: PIISource,
43
+ redactors: Redactor[],
44
+ ): T {
45
+ if (Array.isArray(value)) {
46
+ return value.map((v) =>
47
+ _cleanStringPII<typeof v>(v, source, redactors),
48
+ ) as T;
49
+ }
50
+
51
+ if (typeof value !== "string") {
52
+ return value;
53
+ }
54
+
55
+ let kind: "string" | "url" = "string";
56
+ let decodedValue: string = value;
57
+
58
+ if (_containsEncodedComponents(value)) {
59
+ decodedValue = decodeURIComponent(value);
60
+ kind = "url";
61
+ }
62
+
63
+ return redactors.reduce(
64
+ (redactedValue: string, currentRedactor): string =>
65
+ currentRedactor(redactedValue, source, kind),
66
+ decodedValue,
67
+ ) as T;
68
+ }
69
+
70
+ export function _cleanObjectPII(
71
+ entry: object,
72
+ source: PIISource,
73
+ redactors: Redactor[],
74
+ ): Record<string, AttributeValue> {
75
+ if (!entry) {
76
+ return entry;
77
+ }
78
+
79
+ return Object.fromEntries(
80
+ Object.entries(entry).map(([k, v]) => [
81
+ k,
82
+ _cleanStringPII(v, source, redactors),
83
+ ]),
84
+ );
85
+ }
86
+
87
+ export function _cleanLogBodyPII<T extends AnyValue>(
88
+ value: T,
89
+ redactors: Redactor[],
90
+ ): T {
91
+ if (typeof value === "string") {
92
+ return _cleanStringPII(value, "log", redactors);
93
+ }
94
+
95
+ if (
96
+ typeof value === "number" ||
97
+ typeof value === "boolean" ||
98
+ value == null
99
+ ) {
100
+ return value;
101
+ }
102
+
103
+ if (value instanceof Uint8Array) {
104
+ try {
105
+ const decoded = decoder.decode(value);
106
+ const sanitized = _cleanStringPII(decoded, "log", redactors);
107
+ return encoder.encode(sanitized) as T;
108
+ } catch {
109
+ return value;
110
+ }
111
+ }
112
+
113
+ if (Array.isArray(value)) {
114
+ return value.map((value) => _cleanLogBodyPII(value, redactors)) as T;
115
+ }
116
+
117
+ if (typeof value === "object") {
118
+ const sanitized: AnyValueMap = {};
119
+ for (const [key, val] of Object.entries(value)) {
120
+ sanitized[key] = _cleanLogBodyPII(val, redactors);
121
+ }
122
+ return sanitized as T;
123
+ }
124
+
125
+ return value;
126
+ }
@@ -0,0 +1,58 @@
1
+ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
+
3
+ const EMAIL_REGEX = /[\p{L}\p{N}._%+-]+@((?:[\p{L}\p{N}-]+\.)+[\p{L}]{2,})/giu;
4
+
5
+ /**
6
+ * Redacts all email addresses in the input string and collects metadata.
7
+ *
8
+ * @param {string} value The input string potentially containing email addresses.
9
+ * @returns {{
10
+ * redacted: string,
11
+ * count: number,
12
+ * domains: Record<string, number>
13
+ * }}
14
+ *
15
+ * An object containing:
16
+ * - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
17
+ * - `count`: total number of email addresses redacted
18
+ * - `domains`: a map of domain names to the number of times they were redacted
19
+ */
20
+ function _redactEmails(value: string): {
21
+ redacted: string;
22
+ count: number;
23
+ domains: Record<string, number>;
24
+ } {
25
+ let count = 0;
26
+ const domains: Record<string, number> = {};
27
+
28
+ const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
29
+ count++;
30
+ domains[domain] = (domains[domain] || 0) + 1;
31
+ return "[REDACTED EMAIL]";
32
+ });
33
+
34
+ return { redacted, count, domains };
35
+ }
36
+
37
+ /**
38
+ * Redacts provided input and collects metadata metrics about redacted email domains,
39
+ * data source and kind.
40
+ *
41
+ * @param {string} value The input string potentially containing email addresses.
42
+ * @returns {string} the redacted value
43
+ */
44
+ export const emailRedactor = (value: string, source: string, kind: string) => {
45
+ const { redacted, count, domains } = _redactEmails(value);
46
+
47
+ if (count > 0) {
48
+ for (const [domain, domainCount] of Object.entries(domains)) {
49
+ _getPIICounterRedactionMetric().add(domainCount, {
50
+ pii_type: "email",
51
+ redaction_source: source,
52
+ pii_email_domain: domain,
53
+ pii_format: kind,
54
+ });
55
+ }
56
+ }
57
+ return redacted;
58
+ };
@@ -0,0 +1,12 @@
1
+ import { NodeSDKConfig } from "../../../index.js";
2
+ import { emailRedactor } from "./email.js";
3
+ import { ipRedactor } from "./ip.js";
4
+
5
+ export type Redactor = (value: string, source: string, kind: string) => string;
6
+
7
+ export type RedactorKeys = keyof NonNullable<NodeSDKConfig["detection"]>;
8
+
9
+ export const redactors: Record<RedactorKeys, Redactor> = {
10
+ email: emailRedactor,
11
+ ip: ipRedactor,
12
+ };
@@ -0,0 +1,68 @@
1
+ import { _getPIICounterRedactionMetric } from "../../shared-metrics.js";
2
+
3
+ // Generous IP address matchers (might match some invalid addresses like 192.168.01.1)
4
+ const IPV4_REGEX =
5
+ /(?<!\d)(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?!\d)/gi;
6
+ const IPV6_REGEX =
7
+ /(?<![0-9a-f:])((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?![0-9a-f:])/gi;
8
+
9
+ /**
10
+ * Redacts all ip addresses in the input string and collects metadata.
11
+ *
12
+ * @param {string} value The input string potentially containing ip addresses.
13
+ * @returns {{
14
+ * redacted: string,
15
+ * count: number,
16
+ * domains: Record<string, number>
17
+ * }}
18
+ *
19
+ * An object containing:
20
+ * - `redacted`: the string with IP addresses replaced by `[REDACTED IPV*]`
21
+ * - `counters`: total number of addresses redacted by IPv* type
22
+ * - `domains`: a map of domain names to the number of times they were redacted
23
+ */
24
+ function _redactIps(value: string): {
25
+ redacted: string;
26
+ counters: Record<string, number>;
27
+ } {
28
+ const counters: Record<string, number> = {};
29
+ const redacted = value
30
+ .replace(IPV4_REGEX, () => {
31
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
32
+ return "[REDACTED IPV4]";
33
+ })
34
+ .replace(IPV6_REGEX, () => {
35
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
36
+ return "[REDACTED IPV6]";
37
+ });
38
+ return { redacted, counters };
39
+ }
40
+
41
+ /**
42
+ * Redacts provided input and collects metadata metrics about redacted IPs,
43
+ * data source and kind.
44
+ *
45
+ * @param {string} value The input string potentially containing IP addresses.
46
+ * @param {string} source The source of the attribute being redacted (log, span, metric).
47
+ * @param {string} kind The type of the data structure containing the PII
48
+ * @returns {string} the redacted value
49
+ */
50
+ export const ipRedactor = (
51
+ value: string,
52
+ source: string,
53
+ kind: string,
54
+ ): string => {
55
+ const { redacted, counters } = _redactIps(value);
56
+
57
+ Object.entries(counters).forEach(([type, counter]) => {
58
+ if (counter > 0) {
59
+ _getPIICounterRedactionMetric().add(counter, {
60
+ pii_type: type,
61
+ redaction_source: source,
62
+ pii_format: kind,
63
+ });
64
+ }
65
+ });
66
+
67
+ return redacted;
68
+ };
@@ -1,6 +1,6 @@
1
1
  import { Attributes, Counter } from "@opentelemetry/api";
2
2
  import { getMetric } from "../metrics.js";
3
- import { PIISource } from "./pii-detection.js";
3
+ import { PIISource } from "./redaction/pii-detection.js";
4
4
 
5
5
  interface RedactionMetric extends Attributes {
6
6
  /** Type of PII redacted (e.g., "email", "phone"). */
@@ -1,4 +1,4 @@
1
- import { LogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
1
+ import { SdkLogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
2
2
  import { Context } from "@opentelemetry/api";
3
3
  import { SignalAttributeValue } from "../index.js";
4
4
 
@@ -18,7 +18,7 @@ export class EnrichLogProcessor implements LogRecordProcessor {
18
18
  forceFlush(): Promise<void> {
19
19
  return Promise.resolve();
20
20
  }
21
- onEmit(logRecord: LogRecord, _context?: Context): void {
21
+ onEmit(logRecord: SdkLogRecord, _context?: Context): void {
22
22
  if (this._spanAttributes) {
23
23
  for (const [key, value] of Object.entries(this._spanAttributes)) {
24
24
  logRecord.setAttribute(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,26 +22,26 @@
22
22
  "@grpc/grpc-js": "^1.13.4",
23
23
  "@opentelemetry/api": "^1.9.0",
24
24
  "@opentelemetry/api-logs": "^0.203.0",
25
- "@opentelemetry/auto-instrumentations-node": "^0.60.1",
25
+ "@opentelemetry/auto-instrumentations-node": "^0.62.1",
26
26
  "@opentelemetry/core": "^2.0.1",
27
- "@opentelemetry/exporter-logs-otlp-grpc": "^0.202.0",
28
- "@opentelemetry/exporter-logs-otlp-http": "^0.202.0",
29
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
30
- "@opentelemetry/exporter-metrics-otlp-http": "^0.202.0",
31
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0",
32
- "@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
33
- "@opentelemetry/instrumentation": "^0.202.0",
34
- "@opentelemetry/otlp-exporter-base": "^0.202.0",
27
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
28
+ "@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
29
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0",
30
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0",
31
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
32
+ "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
33
+ "@opentelemetry/instrumentation": "^0.203.0",
34
+ "@opentelemetry/otlp-exporter-base": "^0.203.0",
35
35
  "@opentelemetry/resources": "^2.0.1",
36
- "@opentelemetry/sdk-logs": "^0.202.0",
36
+ "@opentelemetry/sdk-logs": "^0.203.0",
37
37
  "@opentelemetry/sdk-metrics": "^2.0.1",
38
- "@opentelemetry/sdk-node": "^0.202.0",
38
+ "@opentelemetry/sdk-node": "^0.203.0",
39
39
  "@opentelemetry/sdk-trace-base": "^2.0.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "^24.0.10",
42
+ "@types/node": "^24.3.0",
43
43
  "@vitest/coverage-v8": "^3.2.4",
44
- "tsx": "^4.20.3",
44
+ "tsx": "^4.20.5",
45
45
  "typescript": "^5.8.3",
46
46
  "vitest": "^3.2.4"
47
47
  },
@@ -2,8 +2,9 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
2
2
  import {
3
3
  _cleanStringPII,
4
4
  _cleanLogBodyPII,
5
- } from "../../lib/internals/pii-detection.js";
5
+ } from "../../lib/internals/redaction/pii-detection.js";
6
6
  import * as sharedMetrics from "../../lib/internals/shared-metrics.js";
7
+ import { emailRedactor } from "../../lib/internals/redaction/redactors/email";
7
8
 
8
9
  describe("PII Detection Utils", () => {
9
10
  const mockMetricAdd = vi.fn();
@@ -16,9 +17,9 @@ describe("PII Detection Utils", () => {
16
17
  });
17
18
 
18
19
  describe("_cleanStringPII", () => {
19
- it("redacts plain email", () => {
20
+ it("redacts plain PII", () => {
20
21
  const input = "admin@example.com";
21
- const output = _cleanStringPII(input, "log");
22
+ const output = _cleanStringPII(input, "log", [emailRedactor]);
22
23
 
23
24
  expect(output).toBe("[REDACTED EMAIL]");
24
25
  expect(mockMetricAdd).toHaveBeenCalledWith(
@@ -31,9 +32,9 @@ describe("PII Detection Utils", () => {
31
32
  );
32
33
  });
33
34
 
34
- it("redacts email in URL-encoded string", () => {
35
+ it("redacts PII in URL-encoded string", () => {
35
36
  const input = "user%40gmail.com";
36
- const output = _cleanStringPII(input, "log");
37
+ const output = _cleanStringPII(input, "log", [emailRedactor]);
37
38
 
38
39
  expect(output).toBe("[REDACTED EMAIL]");
39
40
  expect(mockMetricAdd).toHaveBeenCalledWith(
@@ -45,9 +46,9 @@ describe("PII Detection Utils", () => {
45
46
  );
46
47
  });
47
48
 
48
- it("handles strings without email unchanged", () => {
49
+ it("handles strings without PII unchanged", () => {
49
50
  const input = "hello world";
50
- const output = _cleanStringPII(input, "log");
51
+ const output = _cleanStringPII(input, "log", [emailRedactor]);
51
52
 
52
53
  expect(output).toBe("hello world");
53
54
  expect(mockMetricAdd).not.toHaveBeenCalled();
@@ -55,23 +56,25 @@ describe("PII Detection Utils", () => {
55
56
 
56
57
  it("handles array of strings", () => {
57
58
  const input = ["one@gmail.com", "two@example.com"];
58
- const output = _cleanStringPII(input, "log");
59
+ const output = _cleanStringPII(input, "log", [emailRedactor]);
59
60
 
60
61
  expect(output).toEqual(["[REDACTED EMAIL]", "[REDACTED EMAIL]"]);
61
62
  expect(mockMetricAdd).toHaveBeenCalledTimes(2);
62
63
  });
63
64
 
64
65
  it("ignores non-string input", () => {
65
- expect(_cleanStringPII(1234, "trace")).toBe(1234);
66
- expect(_cleanStringPII(true, "trace")).toBe(true);
67
- expect(_cleanStringPII(undefined, "trace")).toBeUndefined();
66
+ expect(_cleanStringPII(1234, "trace", [emailRedactor])).toBe(1234);
67
+ expect(_cleanStringPII(true, "trace", [emailRedactor])).toBe(true);
68
+ expect(
69
+ _cleanStringPII(undefined, "trace", [emailRedactor]),
70
+ ).toBeUndefined();
68
71
  expect(mockMetricAdd).not.toHaveBeenCalled();
69
72
  });
70
73
  });
71
74
 
72
75
  describe("_cleanLogBodyPII", () => {
73
- it("cleans string email", () => {
74
- const result = _cleanLogBodyPII("demo@abc.com");
76
+ it("cleans string PII", () => {
77
+ const result = _cleanLogBodyPII("demo@abc.com", [emailRedactor]);
75
78
  expect(result).toBe("[REDACTED EMAIL]");
76
79
  });
77
80
 
@@ -86,7 +89,7 @@ describe("PII Detection Utils", () => {
86
89
  status: "active",
87
90
  };
88
91
 
89
- const result = _cleanLogBodyPII(input);
92
+ const result = _cleanLogBodyPII(input, [emailRedactor]);
90
93
 
91
94
  expect(result).toEqual({
92
95
  user: {
@@ -102,7 +105,7 @@ describe("PII Detection Utils", () => {
102
105
  it("cleans Uint8Array input", () => {
103
106
  const str = "admin@gmail.com";
104
107
  const buffer = new TextEncoder().encode(str);
105
- const result = _cleanLogBodyPII(buffer);
108
+ const result = _cleanLogBodyPII(buffer, [emailRedactor]);
106
109
  const decoded = new TextDecoder().decode(result as Uint8Array);
107
110
 
108
111
  expect(decoded).toBe("[REDACTED EMAIL]");
@@ -110,7 +113,7 @@ describe("PII Detection Utils", () => {
110
113
 
111
114
  it("skips malformed Uint8Array decode", () => {
112
115
  const corrupted = new Uint8Array([0xff, 0xfe, 0xfd]);
113
- const result = _cleanLogBodyPII(corrupted);
116
+ const result = _cleanLogBodyPII(corrupted, [emailRedactor]);
114
117
 
115
118
  // Should return a Uint8Array, but unmodified/redaction should not happen
116
119
  expect(result).toBeInstanceOf(Uint8Array);
@@ -118,11 +121,10 @@ describe("PII Detection Utils", () => {
118
121
  });
119
122
 
120
123
  it("cleans arrays of values", () => {
121
- const result = _cleanLogBodyPII([
122
- "bob@abc.com",
123
- 123,
124
- { nested: "jane@example.com" },
125
- ]);
124
+ const result = _cleanLogBodyPII(
125
+ ["bob@abc.com", 123, { nested: "jane@example.com" }],
126
+ [emailRedactor],
127
+ );
126
128
 
127
129
  expect(result).toEqual([
128
130
  "[REDACTED EMAIL]",
@@ -132,10 +134,10 @@ describe("PII Detection Utils", () => {
132
134
  });
133
135
 
134
136
  it("passes null and boolean through", () => {
135
- expect(_cleanLogBodyPII(null)).toBeNull();
136
- expect(_cleanLogBodyPII(undefined)).toBeUndefined();
137
- expect(_cleanLogBodyPII(true)).toBe(true);
138
- expect(_cleanLogBodyPII(false)).toBe(false);
137
+ expect(_cleanLogBodyPII(null, [emailRedactor])).toBeNull();
138
+ expect(_cleanLogBodyPII(undefined, [emailRedactor])).toBeUndefined();
139
+ expect(_cleanLogBodyPII(true, [emailRedactor])).toBe(true);
140
+ expect(_cleanLogBodyPII(false, [emailRedactor])).toBe(false);
139
141
  });
140
142
  });
141
143
  });
@@ -0,0 +1,81 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ vi,
6
+ beforeEach,
7
+ beforeAll,
8
+ afterAll,
9
+ } from "vitest";
10
+
11
+ import * as sharedMetrics from "../../../lib/internals/shared-metrics.js";
12
+ import { ipRedactor } from "../../../lib/internals/redaction/redactors/ip";
13
+ import { emailRedactor } from "../../../lib/internals/redaction/redactors/email";
14
+
15
+ describe("Email Redaction utils", () => {
16
+ describe("tracks metrics", () => {
17
+ const mockMetricAdd = vi.fn();
18
+
19
+ beforeEach(() => {
20
+ vi.restoreAllMocks();
21
+ vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
22
+ add: mockMetricAdd,
23
+ });
24
+ });
25
+
26
+ it("redacts plain PII and tracks redaction with metric", () => {
27
+ const input = "admin@example.com";
28
+ const output = emailRedactor(input, "log", "string");
29
+
30
+ expect(output).toBe("[REDACTED EMAIL]");
31
+ expect(mockMetricAdd).toHaveBeenCalledWith(
32
+ 1,
33
+ expect.objectContaining({
34
+ pii_email_domain: "example.com",
35
+ pii_type: "email",
36
+ redaction_source: "log",
37
+ }),
38
+ );
39
+ });
40
+
41
+ it("handles strings without PII unchanged", () => {
42
+ const input = "hello world";
43
+ const output = ipRedactor(input, "log", "string");
44
+
45
+ expect(output).toBe("hello world");
46
+ expect(mockMetricAdd).not.toHaveBeenCalled();
47
+ });
48
+ });
49
+
50
+ describe("Redacts email addresses", () => {
51
+ beforeAll(() => {
52
+ vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
53
+ add: vi.fn(),
54
+ });
55
+ });
56
+
57
+ afterAll(() => {
58
+ vi.restoreAllMocks();
59
+ });
60
+
61
+ it.each`
62
+ value | expectedRedactedValue
63
+ ${"user+tag@example.com"} | ${"[REDACTED EMAIL]"}
64
+ ${"user.name+tag+sorting@example.com"} | ${"[REDACTED EMAIL]"}
65
+ ${"x@example.museum"} | ${"[REDACTED EMAIL]"}
66
+ ${"a.b-c_d@example.co.uk"} | ${"[REDACTED EMAIL]"}
67
+ ${"üser@example.de"} | ${"[REDACTED EMAIL]"}
68
+ ${"john.doe@xn--exmple-cua.com"} | ${"[REDACTED EMAIL]"}
69
+ ${"üser@example.de"} | ${"[REDACTED EMAIL]"}
70
+ ${"plainaddress"} | ${"plainaddress"}
71
+ ${"@missinglocal.org"} | ${"@missinglocal.org"}
72
+ ${"user@invalid_domain.com"} | ${"user@invalid_domain.com"}
73
+ `(
74
+ "returns $expectedRedactedValue for value '$value'",
75
+ async ({ value, expectedRedactedValue }: Record<string, string>) => {
76
+ const result = emailRedactor(value, "log", "string");
77
+ expect(result).toBe(expectedRedactedValue);
78
+ },
79
+ );
80
+ });
81
+ });
@@ -0,0 +1,89 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ vi,
6
+ beforeEach,
7
+ beforeAll,
8
+ afterAll,
9
+ } from "vitest";
10
+
11
+ import * as sharedMetrics from "../../../lib/internals/shared-metrics.js";
12
+ import { ipRedactor } from "../../../lib/internals/redaction/redactors/ip";
13
+
14
+ describe("IP Redaction utils", () => {
15
+ describe("tracks metrics", () => {
16
+ const mockMetricAdd = vi.fn();
17
+
18
+ beforeEach(() => {
19
+ vi.restoreAllMocks();
20
+ vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
21
+ add: mockMetricAdd,
22
+ });
23
+ });
24
+
25
+ it("redacts plain PII and tracks redaction with metric", () => {
26
+ const input = "255.255.255.255";
27
+ const output = ipRedactor(input, "log", "string");
28
+
29
+ expect(output).toBe("[REDACTED IPV4]");
30
+ expect(mockMetricAdd).toHaveBeenCalledWith(
31
+ 1,
32
+ expect.objectContaining({
33
+ pii_format: "string",
34
+ pii_type: "IPv4",
35
+ redaction_source: "log",
36
+ }),
37
+ );
38
+ });
39
+
40
+ it("handles strings without PII unchanged", () => {
41
+ const input = "hello world";
42
+ const output = ipRedactor(input, "log", "string");
43
+
44
+ expect(output).toBe("hello world");
45
+ expect(mockMetricAdd).not.toHaveBeenCalled();
46
+ });
47
+ });
48
+
49
+ describe("Redacts IPv4 and IPv6", () => {
50
+ beforeAll(() => {
51
+ vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
52
+ add: vi.fn(),
53
+ });
54
+ });
55
+
56
+ afterAll(() => {
57
+ vi.restoreAllMocks();
58
+ });
59
+
60
+ it.each`
61
+ value | expectedRedactedValue
62
+ ${"hello world"} | ${"hello world"}
63
+ ${"hello 127.0.0.1"} | ${"hello [REDACTED IPV4]"}
64
+ ${"127.0.0.1, hello!"} | ${"[REDACTED IPV4], hello!"}
65
+ ${"127.0.0.1,127.0.0.1"} | ${"[REDACTED IPV4],[REDACTED IPV4]"}
66
+ ${"127.0.0.1127.0.0.1"} | ${"127.0.0.1127.0.0.1"}
67
+ ${"256.1.1.1"} | ${"256.1.1.1"}
68
+ ${"0.0.0.0!"} | ${"[REDACTED IPV4]!"}
69
+ ${"text0.0.0.0"} | ${"text[REDACTED IPV4]"}
70
+ ${"0.0.text0.0"} | ${"0.0.text0.0"}
71
+ ${"2001:0db8::1"} | ${"[REDACTED IPV6]"}
72
+ ${"::1"} | ${"[REDACTED IPV6]"}
73
+ ${"text::1"} | ${"text[REDACTED IPV6]"}
74
+ ${"::1text"} | ${"[REDACTED IPV6]text"}
75
+ ${"sentence ending with f::1"} | ${"sentence ending with [REDACTED IPV6]"}
76
+ ${"sentence ending with :::1"} | ${"sentence ending with :::1"}
77
+ ${"2001:0DB8:85A3:0000:0000:8A2E:0370:7334::1"} | ${"2001:0DB8:85A3:0000:0000:8A2E:0370:7334::1"}
78
+ ${"2001:0DB8:85A3:0000:text:8A2E:0370:7334"} | ${"2001:0DB8:85A3:0000:text:8A2E:0370:7334"}
79
+ ${"2001:0db8::12001:0db8::1"} | ${"2001:0db8::12001:0db8::1"}
80
+ ${"2001:0db8::1,2001:0db8::1"} | ${"[REDACTED IPV6],[REDACTED IPV6]"}
81
+ `(
82
+ "returns $expectedRedactedValue for value '$value'",
83
+ async ({ value, expectedRedactedValue }: Record<string, string>) => {
84
+ const result = ipRedactor(value, "log", "string");
85
+ expect(result).toBe(expectedRedactedValue);
86
+ },
87
+ );
88
+ });
89
+ });
@@ -1,5 +1,5 @@
1
1
  import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
- import * as piiDetection from "../../lib/internals/pii-detection.js";
2
+ import * as piiDetection from "../../lib/internals/redaction/pii-detection.js";
3
3
  import { getActiveSpan } from "../../lib/traces.js";
4
4
  import { MockSpan } from "../utils/mock-signals.js";
5
5
  import { setNodeSdkConfig } from "../../lib/config-manager.js";
package/vitest.config.ts CHANGED
@@ -27,10 +27,10 @@ export default defineConfig({
27
27
  test: {
28
28
  include: [
29
29
  "**/test/*.test.ts",
30
- "**/test/processor/*.test.ts",
31
- "**/test/traces/*.test.ts",
32
- "**/test/internals/*.test.ts",
33
- "**/test/exporter/*.test.ts",
30
+ "**/test/processor/**/*.test.ts",
31
+ "**/test/traces/**/*.test.ts",
32
+ "**/test/internals/**/*.test.ts",
33
+ "**/test/exporter/**/*.test.ts",
34
34
  ],
35
35
  name: "unit",
36
36
  },