@ogcio/o11y-sdk-react 0.3.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-react@v0.3.0...@ogcio/o11y-sdk-react@v0.4.0) (2025-09-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * o11y react-sdk IP pii redaction AB[#30758](https://github.com/ogcio/o11y/issues/30758) ([#196](https://github.com/ogcio/o11y/issues/196)) ([d6bb83f](https://github.com/ogcio/o11y/commit/d6bb83fa425ffdf3fb023ec62caeef070995d07a))
9
+
10
+
11
+ ### Miscellaneous Chores
12
+
13
+ * **deps:** bump the root-deps group across 1 directory with 16 updates ([#193](https://github.com/ogcio/o11y/issues/193)) ([c523565](https://github.com/ogcio/o11y/commit/c523565a6ba7f327473e8d93afc7d532ac220457))
14
+ * **deps:** bump the root-deps group across 3 directories with 34 updates ([#188](https://github.com/ogcio/o11y/issues/188)) ([c0059f3](https://github.com/ogcio/o11y/commit/c0059f33cbb5c39cf6df0f4640604796dbbd3f57))
15
+
3
16
  ## [0.3.0](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-react@v0.2.0...@ogcio/o11y-sdk-react@v0.3.0) (2025-07-31)
4
17
 
5
18
 
@@ -38,10 +38,15 @@ export interface FaroSDKConfig extends SDKConfig {
38
38
  */
39
39
  detection?: {
40
40
  /**
41
- * Redact email address
41
+ * Redact email addresses
42
42
  * @default true
43
43
  */
44
44
  email?: boolean;
45
+ /**
46
+ * Redact ip addresses
47
+ * @default true
48
+ */
49
+ ip?: boolean;
45
50
  };
46
51
  }
47
52
  export type SDKCollectorMode = "single" | "batch";
@@ -6,6 +6,7 @@ import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-docu
6
6
  import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
7
7
  import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
8
8
  import { _beforeSend } from "./internals/hooks.js";
9
+ import { redactors, } from "./internals/redaction/redactors/index.js";
9
10
  export default function buildFaroInstrumentation(config) {
10
11
  if (!config) {
11
12
  console.warn("observability config not set. Skipping Faro OpenTelemetry instrumentation.");
@@ -19,14 +20,11 @@ export default function buildFaroInstrumentation(config) {
19
20
  console.error("collectorUrl does not use a valid format. Skipping Faro OpenTelemetry instrumentation.");
20
21
  return;
21
22
  }
22
- if (!config.detection) {
23
- config.detection = {
24
- email: true,
25
- };
26
- }
27
- if (config.detection.email === undefined) {
28
- config.detection.email = true;
29
- }
23
+ const redactorsChain = Object.entries(redactors)
24
+ .filter(([key]) => {
25
+ return config.detection?.[key] !== false;
26
+ })
27
+ .map(([_, value]) => value);
30
28
  try {
31
29
  diag.setLogger(new DiagConsoleLogger(), config.diagLogLevel
32
30
  ? DiagLogLevel[config.diagLogLevel]
@@ -39,7 +37,7 @@ export default function buildFaroInstrumentation(config) {
39
37
  batching: {
40
38
  enabled: !(config.collectorMode === "single"),
41
39
  },
42
- beforeSend: config.detection.email ? _beforeSend : undefined,
40
+ beforeSend: _beforeSend(redactorsChain),
43
41
  instrumentations: [
44
42
  ...getWebInstrumentations({
45
43
  captureConsole: true,
@@ -1,2 +1,3 @@
1
1
  import { APIEvent, TransportItem } from "@grafana/faro-web-sdk";
2
- export declare function _beforeSend(item: TransportItem<APIEvent>): TransportItem<APIEvent> | null;
2
+ import { Redactor } from "./redaction/redactors/index.js";
3
+ export declare const _beforeSend: (redactors: Redactor[]) => (item: TransportItem<APIEvent>) => TransportItem<APIEvent> | null;
@@ -1,32 +1,34 @@
1
- import { _cleanStringPII } from "./pii-detection.js";
2
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
- function _cleanObjectPII(data, type) {
1
+ import { _cleanStringPII } from "./redaction/pii-detection.js";
2
+ function _cleanObjectPII(data, type, redactors) {
4
3
  if (!data) {
5
4
  return;
6
5
  }
7
6
  if (Array.isArray(data)) {
8
7
  data.forEach((item, index) => {
9
8
  if (typeof item === "string") {
10
- data[index] = _cleanStringPII(data[index], type);
9
+ data[index] = _cleanStringPII(data[index], type, redactors);
11
10
  return;
12
11
  }
13
12
  if (typeof item === "object") {
14
- _cleanObjectPII(item, type);
13
+ _cleanObjectPII(item, type, redactors);
15
14
  }
16
15
  });
17
16
  }
18
17
  if (typeof data === "object") {
19
18
  for (const key in data) {
20
19
  if (typeof data[key] === "string") {
21
- data[key] = _cleanStringPII(data[key], type);
20
+ data[key] = _cleanStringPII(data[key], type, redactors);
22
21
  }
23
22
  if (typeof data[key] === "object") {
24
- _cleanObjectPII(data[key], type);
23
+ _cleanObjectPII(data[key], type, redactors);
25
24
  }
26
25
  }
27
26
  }
28
27
  }
29
- export function _beforeSend(item) {
30
- _cleanObjectPII(item, item.type);
28
+ export const _beforeSend = (redactors) => (item) => {
29
+ if (redactors.length === 0) {
30
+ return item;
31
+ }
32
+ _cleanObjectPII(item, item.type, redactors);
31
33
  return item;
32
- }
34
+ };
@@ -0,0 +1,19 @@
1
+ import { TransportItemType } from "@grafana/faro-web-sdk";
2
+ import { Redactor } from "./redactors/index.js";
3
+ /**
4
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
5
+ *
6
+ * If the string is URL-encoded, it will be decoded before redaction.
7
+ * Metrics are emitted for:
8
+ * - each domain found in redacted email addresses.
9
+ * - IPv4|IPv6 addresses redacted.
10
+ *
11
+ * @template T
12
+ *
13
+ * @param {string} value - The input value to sanitize.
14
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
15
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
16
+ *
17
+ * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
18
+ */
19
+ export declare function _cleanStringPII(value: string, source: TransportItemType, redactors: Redactor[]): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Checks whether a string contains URI-encoded components.
3
+ *
4
+ * @param {string} value - The string to inspect.
5
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
6
+ */
7
+ function _containsEncodedComponents(value) {
8
+ try {
9
+ return decodeURI(value) !== decodeURIComponent(value);
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ /**
16
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
17
+ *
18
+ * If the string is URL-encoded, it will be decoded before redaction.
19
+ * Metrics are emitted for:
20
+ * - each domain found in redacted email addresses.
21
+ * - IPv4|IPv6 addresses redacted.
22
+ *
23
+ * @template T
24
+ *
25
+ * @param {string} value - The input value to sanitize.
26
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
27
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
28
+ *
29
+ * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
30
+ */
31
+ export function _cleanStringPII(value, source, redactors) {
32
+ let kind = "string";
33
+ let decodedValue = value;
34
+ if (_containsEncodedComponents(value)) {
35
+ decodedValue = decodeURIComponent(value);
36
+ kind = "url";
37
+ }
38
+ return redactors.reduce((redactedValue, currentRedactor) => currentRedactor(redactedValue, source, kind), decodedValue);
39
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Redacts provided input and collects metadata metrics about redacted email domains,
3
+ * data source and kind.
4
+ *
5
+ * @param {string} value The input string potentially containing email addresses.
6
+ * @returns {string} the redacted value
7
+ */
8
+ export declare const emailRedactor: (value: string, source: string, kind: string) => string;
@@ -1,5 +1,5 @@
1
1
  import { faro } from "@grafana/faro-web-sdk";
2
- const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-z]{2,})/gi;
2
+ const EMAIL_REGEX = /[\p{L}\p{N}._%+-]+@((?:[\p{L}\p{N}-]+\.)+[\p{L}]{2,})/giu;
3
3
  /**
4
4
  * Redacts all email addresses in the input string and collects metadata.
5
5
  *
@@ -26,37 +26,14 @@ function _redactEmails(value) {
26
26
  return { redacted, count, domains };
27
27
  }
28
28
  /**
29
- * Checks whether a string contains URI-encoded components.
29
+ * Redacts provided input and collects metadata metrics about redacted email domains,
30
+ * data source and kind.
30
31
  *
31
- * @param {string} value - The string to inspect.
32
- * @returns {boolean} `true` if the string is encoded, `false` otherwise.
33
- */
34
- function _containsEncodedComponents(value) {
35
- try {
36
- return decodeURI(value) !== decodeURIComponent(value);
37
- }
38
- catch {
39
- return false;
40
- }
41
- }
42
- /**
43
- * Cleans a string by redacting email addresses and emitting metrics for PII.
44
- *
45
- * If the string is URL-encoded, it will be decoded before redaction.
46
- * Metrics are emitted for each domain found in redacted email addresses.
47
- *
48
- * @param {string} value - The input string to sanitize.
49
- * @param {TransportItemType} source - The source context of the input.
50
- * @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
32
+ * @param {string} value The input string potentially containing email addresses.
33
+ * @returns {string} the redacted value
51
34
  */
52
- export function _cleanStringPII(value, source) {
53
- let kind = "string";
54
- let decodedValue = value;
55
- if (_containsEncodedComponents(value)) {
56
- decodedValue = decodeURIComponent(value);
57
- kind = "url";
58
- }
59
- const { redacted, count, domains } = _redactEmails(decodedValue);
35
+ export const emailRedactor = (value, source, kind) => {
36
+ const { redacted, count, domains } = _redactEmails(value);
60
37
  if (count > 0) {
61
38
  for (const [domain, domainCount] of Object.entries(domains)) {
62
39
  faro.api.pushMeasurement({
@@ -75,4 +52,4 @@ export function _cleanStringPII(value, source) {
75
52
  }
76
53
  }
77
54
  return redacted;
78
- }
55
+ };
@@ -0,0 +1,4 @@
1
+ import { type FaroSDKConfig } from "../../../index.js";
2
+ export type Redactor = (value: string, source: string, kind: string) => string;
3
+ export type RedactorKeys = keyof NonNullable<FaroSDKConfig["detection"]>;
4
+ export declare const redactors: Record<RedactorKeys, Redactor>;
@@ -0,0 +1,6 @@
1
+ import { emailRedactor } from "./email.js";
2
+ import { ipRedactor } from "./ip.js";
3
+ export const redactors = {
4
+ email: emailRedactor,
5
+ ip: ipRedactor,
6
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Redacts provided input and collects metadata metrics about redacted IPs,
3
+ * data source and kind.
4
+ *
5
+ * @param {string} value The input string potentially containing IP addresses.
6
+ * @param {string} source The source of the attribute being redacted (log, span, metric).
7
+ * @param {string} kind The type of the data structure containing the PII
8
+ * @returns {string} the redacted value
9
+ */
10
+ export declare const ipRedactor: (value: string, source: string, kind: string) => string;
@@ -0,0 +1,61 @@
1
+ import { faro } from "@grafana/faro-web-sdk";
2
+ // Generous IP address matchers (might match some invalid addresses like 192.168.01.1)
3
+ const IPV4_REGEX = /(?<!\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;
4
+ const IPV6_REGEX = /(?<![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;
5
+ /**
6
+ * Redacts all ip addresses in the input string and collects metadata.
7
+ *
8
+ * @param {string} value The input string potentially containing ip 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 IP addresses replaced by `[REDACTED IPV*]`
17
+ * - `counters`: total number of addresses redacted by IPv* type
18
+ * - `domains`: a map of domain names to the number of times they were redacted
19
+ */
20
+ function _redactIps(value) {
21
+ const counters = {};
22
+ const redacted = value
23
+ .replace(IPV4_REGEX, () => {
24
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
25
+ return "[REDACTED IPV4]";
26
+ })
27
+ .replace(IPV6_REGEX, () => {
28
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
29
+ return "[REDACTED IPV6]";
30
+ });
31
+ return { redacted, counters };
32
+ }
33
+ /**
34
+ * Redacts provided input and collects metadata metrics about redacted IPs,
35
+ * data source and kind.
36
+ *
37
+ * @param {string} value The input string potentially containing IP addresses.
38
+ * @param {string} source The source of the attribute being redacted (log, span, metric).
39
+ * @param {string} kind The type of the data structure containing the PII
40
+ * @returns {string} the redacted value
41
+ */
42
+ export const ipRedactor = (value, source, kind) => {
43
+ const { redacted, counters } = _redactIps(value);
44
+ Object.entries(counters).forEach(([type, counter]) => {
45
+ if (counter > 0) {
46
+ faro.api.pushMeasurement({
47
+ type: "faro_o11y_pii_redaction",
48
+ values: {
49
+ redacted: counter,
50
+ },
51
+ }, {
52
+ context: {
53
+ pii_type: type,
54
+ redaction_source: source,
55
+ pii_format: kind,
56
+ },
57
+ });
58
+ }
59
+ });
60
+ return redacted;
61
+ };
package/lib/index.ts CHANGED
@@ -39,10 +39,15 @@ export interface FaroSDKConfig extends SDKConfig {
39
39
  */
40
40
  detection?: {
41
41
  /**
42
- * Redact email address
42
+ * Redact email addresses
43
43
  * @default true
44
44
  */
45
45
  email?: boolean;
46
+ /**
47
+ * Redact ip addresses
48
+ * @default true
49
+ */
50
+ ip?: boolean;
46
51
  };
47
52
  }
48
53
 
@@ -13,6 +13,10 @@ import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
13
13
  import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
14
14
  import type { FaroSDKConfig } from "./index.js";
15
15
  import { _beforeSend } from "./internals/hooks.js";
16
+ import {
17
+ RedactorKeys,
18
+ redactors,
19
+ } from "./internals/redaction/redactors/index.js";
16
20
 
17
21
  export default function buildFaroInstrumentation(
18
22
  config?: FaroSDKConfig,
@@ -38,16 +42,11 @@ export default function buildFaroInstrumentation(
38
42
  return;
39
43
  }
40
44
 
41
- if (!config.detection) {
42
- config.detection = {
43
- email: true,
44
- };
45
- }
46
-
47
- if (config.detection.email === undefined) {
48
- config.detection.email = true;
49
- }
50
-
45
+ const redactorsChain = Object.entries(redactors)
46
+ .filter(([key]) => {
47
+ return config.detection?.[key as RedactorKeys] !== false;
48
+ })
49
+ .map(([_, value]) => value);
51
50
  try {
52
51
  diag.setLogger(
53
52
  new DiagConsoleLogger(),
@@ -64,7 +63,7 @@ export default function buildFaroInstrumentation(
64
63
  batching: {
65
64
  enabled: !(config.collectorMode === "single"),
66
65
  },
67
- beforeSend: config.detection.email ? _beforeSend : undefined,
66
+ beforeSend: _beforeSend(redactorsChain),
68
67
  instrumentations: [
69
68
  ...getWebInstrumentations({
70
69
  captureConsole: true,
@@ -3,10 +3,12 @@ import {
3
3
  TransportItem,
4
4
  TransportItemType,
5
5
  } from "@grafana/faro-web-sdk";
6
- import { _cleanStringPII } from "./pii-detection.js";
6
+ import { _cleanStringPII } from "./redaction/pii-detection.js";
7
+ import { Redactor } from "./redaction/redactors/index.js";
7
8
 
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- function _cleanObjectPII(data: any, type: TransportItemType) {
9
+ function _cleanObjectPII<
10
+ T extends string | Record<string, unknown> | unknown[] | unknown,
11
+ >(data: T, type: TransportItemType, redactors: Redactor[]): void {
10
12
  if (!data) {
11
13
  return;
12
14
  }
@@ -14,12 +16,12 @@ function _cleanObjectPII(data: any, type: TransportItemType) {
14
16
  if (Array.isArray(data)) {
15
17
  data.forEach((item, index) => {
16
18
  if (typeof item === "string") {
17
- data[index] = _cleanStringPII(data[index], type);
19
+ data[index] = _cleanStringPII(data[index], type, redactors);
18
20
  return;
19
21
  }
20
22
 
21
23
  if (typeof item === "object") {
22
- _cleanObjectPII(item, type);
24
+ _cleanObjectPII<T>(item, type, redactors);
23
25
  }
24
26
  });
25
27
  }
@@ -27,19 +29,27 @@ function _cleanObjectPII(data: any, type: TransportItemType) {
27
29
  if (typeof data === "object") {
28
30
  for (const key in data) {
29
31
  if (typeof data[key] === "string") {
30
- data[key] = _cleanStringPII(data[key], type);
32
+ data[key] = _cleanStringPII(
33
+ data[key],
34
+ type,
35
+ redactors,
36
+ ) as (typeof data)[typeof key];
31
37
  }
32
38
 
33
39
  if (typeof data[key] === "object") {
34
- _cleanObjectPII(data[key], type);
40
+ _cleanObjectPII(data[key], type, redactors);
35
41
  }
36
42
  }
37
43
  }
38
44
  }
39
45
 
40
- export function _beforeSend(
41
- item: TransportItem<APIEvent>,
42
- ): TransportItem<APIEvent> | null {
43
- _cleanObjectPII(item, item.type);
44
- return item;
45
- }
46
+ export const _beforeSend =
47
+ (redactors: Redactor[]) =>
48
+ (item: TransportItem<APIEvent>): TransportItem<APIEvent> | null => {
49
+ if (redactors.length === 0) {
50
+ return item;
51
+ }
52
+
53
+ _cleanObjectPII(item, item.type, redactors);
54
+ return item;
55
+ };
@@ -0,0 +1,52 @@
1
+ import { TransportItemType } from "@grafana/faro-web-sdk";
2
+ import { Redactor } from "./redactors/index.js";
3
+
4
+ /**
5
+ * Checks whether a string contains URI-encoded components.
6
+ *
7
+ * @param {string} value - The string to inspect.
8
+ * @returns {boolean} `true` if the string is encoded, `false` otherwise.
9
+ */
10
+ function _containsEncodedComponents(value: string) {
11
+ try {
12
+ return decodeURI(value) !== decodeURIComponent(value);
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
20
+ *
21
+ * If the string is URL-encoded, it will be decoded before redaction.
22
+ * Metrics are emitted for:
23
+ * - each domain found in redacted email addresses.
24
+ * - IPv4|IPv6 addresses redacted.
25
+ *
26
+ * @template T
27
+ *
28
+ * @param {string} value - The input value to sanitize.
29
+ * @param {"trace" | "log"} source - The source context of the input, used in metrics.
30
+ * @param {Redactor[]} redactors - The string processors containing the redaction logic.
31
+ *
32
+ * @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
33
+ */
34
+ export function _cleanStringPII(
35
+ value: string,
36
+ source: TransportItemType,
37
+ redactors: Redactor[],
38
+ ): string {
39
+ let kind: "string" | "url" = "string";
40
+ let decodedValue = value;
41
+
42
+ if (_containsEncodedComponents(value)) {
43
+ decodedValue = decodeURIComponent(value);
44
+ kind = "url";
45
+ }
46
+
47
+ return redactors.reduce(
48
+ (redactedValue: string, currentRedactor): string =>
49
+ currentRedactor(redactedValue, source, kind),
50
+ decodedValue,
51
+ );
52
+ }
@@ -1,6 +1,6 @@
1
- import { faro, TransportItemType } from "@grafana/faro-web-sdk";
1
+ import { faro } from "@grafana/faro-web-sdk";
2
2
 
3
- const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-z]{2,})/gi;
3
+ const EMAIL_REGEX = /[\p{L}\p{N}._%+-]+@((?:[\p{L}\p{N}-]+\.)+[\p{L}]{2,})/giu;
4
4
 
5
5
  /**
6
6
  * Redacts all email addresses in the input string and collects metadata.
@@ -35,42 +35,14 @@ function _redactEmails(value: string): {
35
35
  }
36
36
 
37
37
  /**
38
- * Checks whether a string contains URI-encoded components.
38
+ * Redacts provided input and collects metadata metrics about redacted email domains,
39
+ * data source and kind.
39
40
  *
40
- * @param {string} value - The string to inspect.
41
- * @returns {boolean} `true` if the string is encoded, `false` otherwise.
42
- */
43
- function _containsEncodedComponents(value: string) {
44
- try {
45
- return decodeURI(value) !== decodeURIComponent(value);
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- /**
52
- * Cleans a string by redacting email addresses and emitting metrics for PII.
53
- *
54
- * If the string is URL-encoded, it will be decoded before redaction.
55
- * Metrics are emitted for each domain found in redacted email addresses.
56
- *
57
- * @param {string} value - The input string to sanitize.
58
- * @param {TransportItemType} source - The source context of the input.
59
- * @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
41
+ * @param {string} value The input string potentially containing email addresses.
42
+ * @returns {string} the redacted value
60
43
  */
61
- export function _cleanStringPII(
62
- value: string,
63
- source: TransportItemType,
64
- ): string {
65
- let kind: "string" | "url" = "string";
66
- let decodedValue = value;
67
-
68
- if (_containsEncodedComponents(value)) {
69
- decodedValue = decodeURIComponent(value);
70
- kind = "url";
71
- }
72
-
73
- const { redacted, count, domains } = _redactEmails(decodedValue);
44
+ export const emailRedactor = (value: string, source: string, kind: string) => {
45
+ const { redacted, count, domains } = _redactEmails(value);
74
46
 
75
47
  if (count > 0) {
76
48
  for (const [domain, domainCount] of Object.entries(domains)) {
@@ -93,4 +65,4 @@ export function _cleanStringPII(
93
65
  }
94
66
  }
95
67
  return redacted;
96
- }
68
+ };
@@ -0,0 +1,12 @@
1
+ import { type FaroSDKConfig } 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<FaroSDKConfig["detection"]>;
8
+
9
+ export const redactors: Record<RedactorKeys, Redactor> = {
10
+ email: emailRedactor,
11
+ ip: ipRedactor,
12
+ };
@@ -0,0 +1,78 @@
1
+ import { faro } from "@grafana/faro-web-sdk";
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
+ faro.api.pushMeasurement(
60
+ {
61
+ type: "faro_o11y_pii_redaction",
62
+ values: {
63
+ redacted: counter,
64
+ },
65
+ },
66
+ {
67
+ context: {
68
+ pii_type: type,
69
+ redaction_source: source,
70
+ pii_format: kind,
71
+ },
72
+ },
73
+ );
74
+ }
75
+ });
76
+
77
+ return redacted;
78
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Opentelemetry standard instrumentation SDK for React based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -19,19 +19,19 @@
19
19
  "author": "team:ogcio/observability",
20
20
  "license": "ISC",
21
21
  "dependencies": {
22
- "@grafana/faro-react": "^1.18.2",
23
- "@grafana/faro-web-sdk": "^1.18.2",
24
- "@grafana/faro-web-tracing": "^1.18.2",
22
+ "@grafana/faro-react": "^1.19.0",
23
+ "@grafana/faro-web-sdk": "^1.19.0",
24
+ "@grafana/faro-web-tracing": "^1.19.0",
25
25
  "@opentelemetry/api": "^1.9.0",
26
26
  "@opentelemetry/core": "2.0.1",
27
- "@opentelemetry/instrumentation-document-load": "^0.47.0",
28
- "@opentelemetry/instrumentation-fetch": "^0.202.0",
29
- "@opentelemetry/instrumentation-user-interaction": "^0.47.0"
27
+ "@opentelemetry/instrumentation-document-load": "^0.48.0",
28
+ "@opentelemetry/instrumentation-fetch": "^0.203.0",
29
+ "@opentelemetry/instrumentation-user-interaction": "^0.48.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/node": "^24.0.10",
32
+ "@types/node": "^24.3.0",
33
33
  "@vitest/coverage-v8": "^3.2.4",
34
- "tsx": "^4.20.3",
34
+ "tsx": "^4.20.5",
35
35
  "typescript": "^5.8.3",
36
36
  "vitest": "^3.2.4"
37
37
  },
@@ -2,7 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import buildFaroInstrumentation from "../lib/instrumentation.faro";
3
3
  import { initializeFaro } from "@grafana/faro-react";
4
4
  import { _beforeSend } from "../lib/internals/hooks";
5
- import { FaroSDKConfig } from "../lib";
5
+ import { redactors } from "../lib/internals/redaction/redactors";
6
+ import * as detectionTools from "../lib/internals/redaction/pii-detection";
6
7
 
7
8
  // Mock dependencies
8
9
  vi.mock("@grafana/faro-react", async () => {
@@ -89,31 +90,35 @@ describe("buildFaroInstrumentation", () => {
89
90
  expect(result).toBeUndefined();
90
91
  });
91
92
 
92
- it("defaults detection.email to true if not provided", () => {
93
+ it("defaults all detection to true if not provided", () => {
93
94
  const config = {
94
95
  ...validConfig,
95
96
  detection: {},
96
97
  };
97
98
  buildFaroInstrumentation(config);
98
- expect(initializeFaro).toHaveBeenCalled();
99
- expect(config.detection.email).toBe(true);
99
+ expect(initializeFaro).toHaveBeenCalledOnce();
100
+ expect(initializeFaro.mock.calls[0][0].beforeSend.toString()).toEqual(
101
+ _beforeSend(Object.values(redactors)).toString(),
102
+ );
100
103
  });
101
104
 
102
105
  it("sets beforeSend if detection.email is true", () => {
103
106
  buildFaroInstrumentation({ ...validConfig, detection: { email: true } });
104
- expect(initializeFaro).toHaveBeenCalledWith(
105
- expect.objectContaining({
106
- beforeSend: _beforeSend,
107
- }),
107
+ expect(initializeFaro).toHaveBeenCalledOnce();
108
+ expect(initializeFaro.mock.calls[0][0].beforeSend.toString()).toEqual(
109
+ _beforeSend(Object.values(redactors)).toString(),
108
110
  );
109
111
  });
110
112
 
111
113
  it("does not set beforeSend if detection.email is false", () => {
112
114
  buildFaroInstrumentation({ ...validConfig, detection: { email: false } });
113
- expect(initializeFaro).toHaveBeenCalledWith(
114
- expect.not.objectContaining({
115
- beforeSend: expect.any(Function),
116
- }),
115
+ expect(initializeFaro).toHaveBeenCalledOnce();
116
+ expect(initializeFaro.mock.calls[0][0].beforeSend.toString()).toEqual(
117
+ _beforeSend(
118
+ Object.entries(redactors)
119
+ .filter(([key]) => key !== "email")
120
+ .map(([, redactor]) => redactor),
121
+ ).toString(),
117
122
  );
118
123
  });
119
124
 
@@ -165,20 +170,28 @@ describe("buildFaroInstrumentation", () => {
165
170
  expect(result).toEqual({ fakeFaro: true });
166
171
  });
167
172
 
168
- it("sets default detection config if detection is not provided", () => {
173
+ it("skips detection if all redactors are set to false", () => {
169
174
  const config = {
170
175
  ...validConfig,
171
- detection: undefined,
176
+ detection: Object.fromEntries(
177
+ Object.keys(redactors).map((key) => [key, false]),
178
+ ),
172
179
  };
173
180
 
174
181
  buildFaroInstrumentation(config);
175
182
 
176
- expect(config.detection).toEqual({ email: true });
177
- expect(initializeFaro).toHaveBeenCalledWith(
178
- expect.objectContaining({
179
- beforeSend: _beforeSend,
180
- }),
181
- );
183
+ const cleanStringMock = vi.spyOn(detectionTools, "_cleanStringPII");
184
+
185
+ expect(initializeFaro).toHaveBeenCalledOnce();
186
+ expect(initializeFaro.mock.calls[0][0].beforeSend).toBeTruthy();
187
+
188
+ initializeFaro.mock.calls[0][0].beforeSend({
189
+ type: "log",
190
+ payload: { name: "eventName", timestamp: "a timestamp" },
191
+ meta: {},
192
+ });
193
+
194
+ expect(cleanStringMock).not.toHaveBeenCalled();
182
195
  });
183
196
 
184
197
  it("handles comma-separated corsTraceHeaders", async () => {
@@ -21,6 +21,7 @@ vi.mock("@grafana/faro-web-sdk", async () => {
21
21
  });
22
22
 
23
23
  import { faro } from "@grafana/faro-web-sdk";
24
+ import { redactors } from "../../lib/internals/redaction/redactors";
24
25
 
25
26
  describe("_beforeSend", () => {
26
27
  beforeEach(() => {
@@ -46,7 +47,7 @@ describe("_beforeSend", () => {
46
47
  },
47
48
  } satisfies TransportItem<LogEvent>;
48
49
 
49
- const redacted: TransportItem<LogEvent> = _beforeSend(
50
+ const redacted: TransportItem<LogEvent> = _beforeSend([redactors.email])(
50
51
  item,
51
52
  ) as TransportItem<LogEvent>;
52
53
 
@@ -73,7 +74,7 @@ describe("_beforeSend", () => {
73
74
  },
74
75
  };
75
76
 
76
- const redacted = _beforeSend(item)!;
77
+ const redacted = _beforeSend([redactors.email])(item)!;
77
78
 
78
79
  expect(redacted.payload.context.messages[1]).toBe(
79
80
  "email: [REDACTED EMAIL]",
@@ -98,7 +99,7 @@ describe("_beforeSend", () => {
98
99
  },
99
100
  };
100
101
 
101
- const redacted = _beforeSend(item)!;
102
+ const redacted = _beforeSend([redactors.email])(item)!;
102
103
 
103
104
  expect(redacted.payload.context.events[0].message).toBe(
104
105
  "contact [REDACTED EMAIL]",
@@ -121,7 +122,7 @@ describe("_beforeSend", () => {
121
122
  meta: null,
122
123
  };
123
124
 
124
- const redacted = _beforeSend(item)!;
125
+ const redacted = _beforeSend([redactors.email])(item)!;
125
126
 
126
127
  expect(redacted.payload.message).toBeNull();
127
128
  expect(redacted.payload.count).toBe(123);
@@ -141,7 +142,7 @@ describe("_beforeSend", () => {
141
142
  },
142
143
  };
143
144
 
144
- const redacted = _beforeSend(item)!;
145
+ const redacted = _beforeSend([redactors.email])(item)!;
145
146
 
146
147
  expect(redacted.payload.message).toBe("contact [REDACTED EMAIL]");
147
148
  expect(faro.api.pushMeasurement).toHaveBeenCalledOnce();
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
2
+
3
+ import { emailRedactor } from "../../../../lib/internals/redaction/redactors/email";
4
+ import { ipRedactor } from "../../../../lib/internals/redaction/redactors/ip";
5
+ import { faro, TransportItemType } from "@grafana/faro-web-sdk";
6
+
7
+ // Mock faro.api.pushMeasurement
8
+ vi.mock("@grafana/faro-web-sdk", async () => {
9
+ const actual = await vi.importActual<any>("@grafana/faro-web-sdk");
10
+ return {
11
+ ...actual,
12
+ faro: {
13
+ api: {
14
+ pushMeasurement: vi.fn(),
15
+ },
16
+ },
17
+ };
18
+ });
19
+
20
+ describe("Email Redaction utils", () => {
21
+ afterAll(() => {
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ describe("tracks metrics", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ it("redacts plain PII and tracks redaction with metric", () => {
31
+ const input = "admin@example.com";
32
+ const output = emailRedactor(input, "log", "string");
33
+
34
+ expect(output).toBe("[REDACTED EMAIL]");
35
+ expect(faro.api.pushMeasurement).toHaveBeenCalledWith(
36
+ {
37
+ type: "faro_o11y_pii_redaction",
38
+ values: { redacted: 1 },
39
+ },
40
+ {
41
+ context: {
42
+ pii_type: "email",
43
+ redaction_source: TransportItemType.LOG,
44
+ pii_email_domain: "example.com",
45
+ pii_format: "string",
46
+ },
47
+ },
48
+ );
49
+ });
50
+
51
+ it("handles strings without PII unchanged", () => {
52
+ const input = "hello world";
53
+ const output = ipRedactor(input, "log", "string");
54
+
55
+ expect(output).toBe("hello world");
56
+ expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
57
+ });
58
+ });
59
+
60
+ describe("Redacts email addresses", () => {
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,86 @@
1
+ import { describe, expect, it, vi, afterAll } from "vitest";
2
+
3
+ import { faro } from "@grafana/faro-web-sdk";
4
+ import { ipRedactor } from "../../../../lib/internals/redaction/redactors/ip";
5
+
6
+ // Mock faro.api.pushMeasurement
7
+ vi.mock("@grafana/faro-web-sdk", async () => {
8
+ const actual = await vi.importActual<any>("@grafana/faro-web-sdk");
9
+ return {
10
+ ...actual,
11
+ faro: {
12
+ api: {
13
+ pushMeasurement: vi.fn(),
14
+ },
15
+ },
16
+ };
17
+ });
18
+
19
+ describe("IP Redaction utils", () => {
20
+ afterAll(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ describe("tracks metrics", () => {
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(faro.api.pushMeasurement).toHaveBeenCalledWith(
31
+ {
32
+ type: "faro_o11y_pii_redaction",
33
+ values: {
34
+ redacted: 1,
35
+ },
36
+ },
37
+ {
38
+ context: {
39
+ pii_format: "string",
40
+ pii_type: "IPv4",
41
+ redaction_source: "log",
42
+ },
43
+ },
44
+ );
45
+ });
46
+
47
+ it("handles strings without PII unchanged", () => {
48
+ const input = "hello world";
49
+ const output = ipRedactor(input, "log", "string");
50
+
51
+ expect(output).toBe("hello world");
52
+ expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
53
+ });
54
+ });
55
+
56
+ describe("Redacts IPv4 and IPv6", () => {
57
+ it.each`
58
+ value | expectedRedactedValue
59
+ ${"hello world"} | ${"hello world"}
60
+ ${"hello 127.0.0.1"} | ${"hello [REDACTED IPV4]"}
61
+ ${"127.0.0.1, hello!"} | ${"[REDACTED IPV4], hello!"}
62
+ ${"127.0.0.1,127.0.0.1"} | ${"[REDACTED IPV4],[REDACTED IPV4]"}
63
+ ${"127.0.0.1127.0.0.1"} | ${"127.0.0.1127.0.0.1"}
64
+ ${"256.1.1.1"} | ${"256.1.1.1"}
65
+ ${"0.0.0.0!"} | ${"[REDACTED IPV4]!"}
66
+ ${"text0.0.0.0"} | ${"text[REDACTED IPV4]"}
67
+ ${"0.0.text0.0"} | ${"0.0.text0.0"}
68
+ ${"2001:0db8::1"} | ${"[REDACTED IPV6]"}
69
+ ${"::1"} | ${"[REDACTED IPV6]"}
70
+ ${"text::1"} | ${"text[REDACTED IPV6]"}
71
+ ${"::1text"} | ${"[REDACTED IPV6]text"}
72
+ ${"sentence ending with f::1"} | ${"sentence ending with [REDACTED IPV6]"}
73
+ ${"sentence ending with :::1"} | ${"sentence ending with :::1"}
74
+ ${"2001:0DB8:85A3:0000:0000:8A2E:0370:7334::1"} | ${"2001:0DB8:85A3:0000:0000:8A2E:0370:7334::1"}
75
+ ${"2001:0DB8:85A3:0000:text:8A2E:0370:7334"} | ${"2001:0DB8:85A3:0000:text:8A2E:0370:7334"}
76
+ ${"2001:0db8::12001:0db8::1"} | ${"2001:0db8::12001:0db8::1"}
77
+ ${"2001:0db8::1,2001:0db8::1"} | ${"[REDACTED IPV6],[REDACTED IPV6]"}
78
+ `(
79
+ "returns $expectedRedactedValue for value '$value'",
80
+ async ({ value, expectedRedactedValue }: Record<string, string>) => {
81
+ const result = ipRedactor(value, "log", "string");
82
+ expect(result).toBe(expectedRedactedValue);
83
+ },
84
+ );
85
+ });
86
+ });
@@ -1,6 +1,6 @@
1
1
  import { TransportItemType } from "@grafana/faro-web-sdk";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import { _cleanStringPII } from "../../lib/internals/pii-detection";
3
+ import { _cleanStringPII } from "../../lib/internals/redaction/pii-detection";
4
4
 
5
5
  // Mock faro.api.pushMeasurement
6
6
  vi.mock("@grafana/faro-web-sdk", async () => {
@@ -16,6 +16,7 @@ vi.mock("@grafana/faro-web-sdk", async () => {
16
16
  });
17
17
 
18
18
  import { faro } from "@grafana/faro-web-sdk";
19
+ import { redactors } from "../../lib/internals/redaction/redactors";
19
20
 
20
21
  describe("_cleanStringPII", () => {
21
22
  beforeEach(() => {
@@ -24,7 +25,9 @@ describe("_cleanStringPII", () => {
24
25
 
25
26
  it("should redact email and call pushMeasurement", () => {
26
27
  const input = "Contact me at alice@example.com.";
27
- const result = _cleanStringPII(input, TransportItemType.LOG);
28
+ const result = _cleanStringPII(input, TransportItemType.LOG, [
29
+ redactors.email,
30
+ ]);
28
31
 
29
32
  expect(result).toBe("Contact me at [REDACTED EMAIL].");
30
33
  expect(faro.api.pushMeasurement).toHaveBeenCalledOnce();
@@ -46,7 +49,9 @@ describe("_cleanStringPII", () => {
46
49
 
47
50
  it("should decode and redact URI-encoded emails", () => {
48
51
  const encoded = "mailto%3Ajohn.doe%40gmail.com";
49
- const result = _cleanStringPII(encoded, TransportItemType.EXCEPTION);
52
+ const result = _cleanStringPII(encoded, TransportItemType.EXCEPTION, [
53
+ redactors.email,
54
+ ]);
50
55
 
51
56
  expect(result).toBe("mailto:[REDACTED EMAIL]");
52
57
  expect(faro.api.pushMeasurement).toHaveBeenCalledOnce();
@@ -54,7 +59,9 @@ describe("_cleanStringPII", () => {
54
59
 
55
60
  it("should return same string when no email present", () => {
56
61
  const input = "Just a normal message.";
57
- const result = _cleanStringPII(input, TransportItemType.LOG);
62
+ const result = _cleanStringPII(input, TransportItemType.LOG, [
63
+ redactors.email,
64
+ ]);
58
65
 
59
66
  expect(result).toBe(input);
60
67
  expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
@@ -62,7 +69,9 @@ describe("_cleanStringPII", () => {
62
69
 
63
70
  it("should return same string when decodeURIComponent fail", () => {
64
71
  const input = "mailto%john.doe%4gmail.com";
65
- const result = _cleanStringPII(input, TransportItemType.LOG);
72
+ const result = _cleanStringPII(input, TransportItemType.LOG, [
73
+ redactors.email,
74
+ ]);
66
75
 
67
76
  expect(result).toBe(input);
68
77
  expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
@@ -1,12 +0,0 @@
1
- import { TransportItemType } from "@grafana/faro-web-sdk";
2
- /**
3
- * Cleans a string by redacting email addresses and emitting metrics for PII.
4
- *
5
- * If the string is URL-encoded, it will be decoded before redaction.
6
- * Metrics are emitted for each domain found in redacted email addresses.
7
- *
8
- * @param {string} value - The input string to sanitize.
9
- * @param {TransportItemType} source - The source context of the input.
10
- * @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
11
- */
12
- export declare function _cleanStringPII(value: string, source: TransportItemType): string;