@ogcio/o11y-sdk-react 0.3.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/lib/index.d.ts +6 -1
- package/dist/lib/instrumentation.faro.js +7 -9
- package/dist/lib/internals/hooks.d.ts +2 -1
- package/dist/lib/internals/hooks.js +12 -10
- package/dist/lib/internals/redaction/pii-detection.d.ts +26 -0
- package/dist/lib/internals/redaction/pii-detection.js +47 -0
- package/dist/lib/internals/redaction/redactors/email.d.ts +8 -0
- package/dist/lib/internals/{pii-detection.js → redaction/redactors/email.js} +8 -31
- package/dist/lib/internals/redaction/redactors/index.d.ts +4 -0
- package/dist/lib/internals/redaction/redactors/index.js +6 -0
- package/dist/lib/internals/redaction/redactors/ip.d.ts +10 -0
- package/dist/lib/internals/redaction/redactors/ip.js +61 -0
- package/lib/index.ts +6 -1
- package/lib/instrumentation.faro.ts +10 -11
- package/lib/internals/hooks.ts +23 -13
- package/lib/internals/redaction/pii-detection.ts +64 -0
- package/lib/internals/{pii-detection.ts → redaction/redactors/email.ts} +9 -37
- package/lib/internals/redaction/redactors/index.ts +12 -0
- package/lib/internals/redaction/redactors/ip.ts +78 -0
- package/package.json +9 -9
- package/test/config.test.ts +33 -20
- package/test/lib/hooks.test.ts +6 -5
- package/test/lib/internals/redactors/email.test.ts +81 -0
- package/test/lib/internals/redactors/ip.test.ts +86 -0
- package/test/lib/pii-detection.test.ts +116 -7
- package/dist/lib/internals/pii-detection.d.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.2](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-react@v0.4.0...@ogcio/o11y-sdk-react@v0.4.2) (2025-09-09)
|
|
4
|
+
|
|
5
|
+
### Miscellaneous Chores
|
|
6
|
+
|
|
7
|
+
* **sdk-react:** propagate url encoding detection to react-sdk AB[#30758](https://github.com/ogcio/o11y/issues/30758) ([#202](https://github.com/ogcio/o11y/issues/202)) ([aaf0d5a](https://github.com/ogcio/o11y/commit/aaf0d5a9b30a186d804fc839a23180891d9ef883))
|
|
8
|
+
|
|
9
|
+
## [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)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* 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))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Miscellaneous Chores
|
|
18
|
+
|
|
19
|
+
* **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))
|
|
20
|
+
* **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))
|
|
21
|
+
|
|
3
22
|
## [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
23
|
|
|
5
24
|
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -38,10 +38,15 @@ export interface FaroSDKConfig extends SDKConfig {
|
|
|
38
38
|
*/
|
|
39
39
|
detection?: {
|
|
40
40
|
/**
|
|
41
|
-
* Redact email
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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,26 @@
|
|
|
1
|
+
import { TransportItemType } from "@grafana/faro-web-sdk";
|
|
2
|
+
import { Redactor } from "./redactors/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether a string contains URI-encoded components.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} value - The string to inspect.
|
|
7
|
+
* @returns {boolean} `true` if the string is encoded, `false` otherwise.
|
|
8
|
+
*/
|
|
9
|
+
export declare function _containsEncodedComponents(value: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
|
|
12
|
+
*
|
|
13
|
+
* If the string is URL-encoded, it will be decoded before redaction.
|
|
14
|
+
* Metrics are emitted for:
|
|
15
|
+
* - each domain found in redacted email addresses.
|
|
16
|
+
* - IPv4|IPv6 addresses redacted.
|
|
17
|
+
*
|
|
18
|
+
* @template T
|
|
19
|
+
*
|
|
20
|
+
* @param {string} value - The input value to sanitize.
|
|
21
|
+
* @param {"trace" | "log"} source - The source context of the input, used in metrics.
|
|
22
|
+
* @param {Redactor[]} redactors - The string processors containing the redaction logic.
|
|
23
|
+
*
|
|
24
|
+
* @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function _cleanStringPII(value: string, source: TransportItemType, redactors: Redactor[]): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
export function _containsEncodedComponents(value) {
|
|
8
|
+
try {
|
|
9
|
+
const decodedURIComponent = decodeURIComponent(value);
|
|
10
|
+
if (decodeURI(value) !== decodedURIComponent) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (value !== decodedURIComponent) {
|
|
14
|
+
return (encodeURIComponent(decodedURIComponent) === value ||
|
|
15
|
+
encodeURI(decodedURIComponent) === value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
|
|
25
|
+
*
|
|
26
|
+
* If the string is URL-encoded, it will be decoded before redaction.
|
|
27
|
+
* Metrics are emitted for:
|
|
28
|
+
* - each domain found in redacted email addresses.
|
|
29
|
+
* - IPv4|IPv6 addresses redacted.
|
|
30
|
+
*
|
|
31
|
+
* @template T
|
|
32
|
+
*
|
|
33
|
+
* @param {string} value - The input value to sanitize.
|
|
34
|
+
* @param {"trace" | "log"} source - The source context of the input, used in metrics.
|
|
35
|
+
* @param {Redactor[]} redactors - The string processors containing the redaction logic.
|
|
36
|
+
*
|
|
37
|
+
* @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
|
|
38
|
+
*/
|
|
39
|
+
export function _cleanStringPII(value, source, redactors) {
|
|
40
|
+
let kind = "string";
|
|
41
|
+
let decodedValue = value;
|
|
42
|
+
if (_containsEncodedComponents(value)) {
|
|
43
|
+
decodedValue = decodeURIComponent(value);
|
|
44
|
+
kind = "url";
|
|
45
|
+
}
|
|
46
|
+
return redactors.reduce((redactedValue, currentRedactor) => currentRedactor(redactedValue, source, kind), decodedValue);
|
|
47
|
+
}
|
|
@@ -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 = /[
|
|
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
|
-
*
|
|
29
|
+
* Redacts provided input and collects metadata metrics about redacted email domains,
|
|
30
|
+
* data source and kind.
|
|
30
31
|
*
|
|
31
|
-
* @param {string} value
|
|
32
|
-
* @returns {
|
|
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
|
|
53
|
-
|
|
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,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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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:
|
|
66
|
+
beforeSend: _beforeSend(redactorsChain),
|
|
68
67
|
instrumentations: [
|
|
69
68
|
...getWebInstrumentations({
|
|
70
69
|
captureConsole: true,
|
package/lib/internals/hooks.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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(
|
|
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
|
|
41
|
-
|
|
42
|
-
): TransportItem<APIEvent> | null {
|
|
43
|
-
|
|
44
|
-
|
|
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,64 @@
|
|
|
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
|
+
export function _containsEncodedComponents(value: string): boolean {
|
|
11
|
+
try {
|
|
12
|
+
const decodedURIComponent = decodeURIComponent(value);
|
|
13
|
+
if (decodeURI(value) !== decodedURIComponent) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value !== decodedURIComponent) {
|
|
18
|
+
return (
|
|
19
|
+
encodeURIComponent(decodedURIComponent) === value ||
|
|
20
|
+
encodeURI(decodedURIComponent) === value
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
|
|
32
|
+
*
|
|
33
|
+
* If the string is URL-encoded, it will be decoded before redaction.
|
|
34
|
+
* Metrics are emitted for:
|
|
35
|
+
* - each domain found in redacted email addresses.
|
|
36
|
+
* - IPv4|IPv6 addresses redacted.
|
|
37
|
+
*
|
|
38
|
+
* @template T
|
|
39
|
+
*
|
|
40
|
+
* @param {string} value - The input value to sanitize.
|
|
41
|
+
* @param {"trace" | "log"} source - The source context of the input, used in metrics.
|
|
42
|
+
* @param {Redactor[]} redactors - The string processors containing the redaction logic.
|
|
43
|
+
*
|
|
44
|
+
* @returns {T} The cleaned string with any configured PII replaced by `[REDACTED PII_TYPE]`.
|
|
45
|
+
*/
|
|
46
|
+
export function _cleanStringPII(
|
|
47
|
+
value: string,
|
|
48
|
+
source: TransportItemType,
|
|
49
|
+
redactors: Redactor[],
|
|
50
|
+
): string {
|
|
51
|
+
let kind: "string" | "url" = "string";
|
|
52
|
+
let decodedValue = value;
|
|
53
|
+
|
|
54
|
+
if (_containsEncodedComponents(value)) {
|
|
55
|
+
decodedValue = decodeURIComponent(value);
|
|
56
|
+
kind = "url";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return redactors.reduce(
|
|
60
|
+
(redactedValue: string, currentRedactor): string =>
|
|
61
|
+
currentRedactor(redactedValue, source, kind),
|
|
62
|
+
decodedValue,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { faro
|
|
1
|
+
import { faro } from "@grafana/faro-web-sdk";
|
|
2
2
|
|
|
3
|
-
const EMAIL_REGEX = /[
|
|
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
|
-
*
|
|
38
|
+
* Redacts provided input and collects metadata metrics about redacted email domains,
|
|
39
|
+
* data source and kind.
|
|
39
40
|
*
|
|
40
|
-
* @param {string} value
|
|
41
|
-
* @returns {
|
|
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
|
|
62
|
-
|
|
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
|
+
"version": "0.4.2",
|
|
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.
|
|
23
|
-
"@grafana/faro-web-sdk": "^1.
|
|
24
|
-
"@grafana/faro-web-tracing": "^1.
|
|
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.
|
|
28
|
-
"@opentelemetry/instrumentation-fetch": "^0.
|
|
29
|
-
"@opentelemetry/instrumentation-user-interaction": "^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
|
|
32
|
+
"@types/node": "^24.3.0",
|
|
33
33
|
"@vitest/coverage-v8": "^3.2.4",
|
|
34
|
-
"tsx": "^4.20.
|
|
34
|
+
"tsx": "^4.20.5",
|
|
35
35
|
"typescript": "^5.8.3",
|
|
36
36
|
"vitest": "^3.2.4"
|
|
37
37
|
},
|
package/test/config.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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).
|
|
99
|
-
expect(
|
|
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).
|
|
105
|
-
|
|
106
|
-
|
|
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).
|
|
114
|
-
|
|
115
|
-
|
|
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("
|
|
173
|
+
it("skips detection if all redactors are set to false", () => {
|
|
169
174
|
const config = {
|
|
170
175
|
...validConfig,
|
|
171
|
-
detection:
|
|
176
|
+
detection: Object.fromEntries(
|
|
177
|
+
Object.keys(redactors).map((key) => [key, false]),
|
|
178
|
+
),
|
|
172
179
|
};
|
|
173
180
|
|
|
174
181
|
buildFaroInstrumentation(config);
|
|
175
182
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 () => {
|
package/test/lib/hooks.test.ts
CHANGED
|
@@ -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,11 @@
|
|
|
1
1
|
import { TransportItemType } from "@grafana/faro-web-sdk";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
_cleanStringPII,
|
|
5
|
+
_containsEncodedComponents,
|
|
6
|
+
} from "../../lib/internals/redaction/pii-detection";
|
|
7
|
+
import { faro } from "@grafana/faro-web-sdk";
|
|
8
|
+
import { redactors } from "../../lib/internals/redaction/redactors";
|
|
4
9
|
|
|
5
10
|
// Mock faro.api.pushMeasurement
|
|
6
11
|
vi.mock("@grafana/faro-web-sdk", async () => {
|
|
@@ -15,8 +20,6 @@ vi.mock("@grafana/faro-web-sdk", async () => {
|
|
|
15
20
|
};
|
|
16
21
|
});
|
|
17
22
|
|
|
18
|
-
import { faro } from "@grafana/faro-web-sdk";
|
|
19
|
-
|
|
20
23
|
describe("_cleanStringPII", () => {
|
|
21
24
|
beforeEach(() => {
|
|
22
25
|
vi.clearAllMocks();
|
|
@@ -24,7 +27,9 @@ describe("_cleanStringPII", () => {
|
|
|
24
27
|
|
|
25
28
|
it("should redact email and call pushMeasurement", () => {
|
|
26
29
|
const input = "Contact me at alice@example.com.";
|
|
27
|
-
const result = _cleanStringPII(input, TransportItemType.LOG
|
|
30
|
+
const result = _cleanStringPII(input, TransportItemType.LOG, [
|
|
31
|
+
redactors.email,
|
|
32
|
+
]);
|
|
28
33
|
|
|
29
34
|
expect(result).toBe("Contact me at [REDACTED EMAIL].");
|
|
30
35
|
expect(faro.api.pushMeasurement).toHaveBeenCalledOnce();
|
|
@@ -46,7 +51,9 @@ describe("_cleanStringPII", () => {
|
|
|
46
51
|
|
|
47
52
|
it("should decode and redact URI-encoded emails", () => {
|
|
48
53
|
const encoded = "mailto%3Ajohn.doe%40gmail.com";
|
|
49
|
-
const result = _cleanStringPII(encoded, TransportItemType.EXCEPTION
|
|
54
|
+
const result = _cleanStringPII(encoded, TransportItemType.EXCEPTION, [
|
|
55
|
+
redactors.email,
|
|
56
|
+
]);
|
|
50
57
|
|
|
51
58
|
expect(result).toBe("mailto:[REDACTED EMAIL]");
|
|
52
59
|
expect(faro.api.pushMeasurement).toHaveBeenCalledOnce();
|
|
@@ -54,7 +61,9 @@ describe("_cleanStringPII", () => {
|
|
|
54
61
|
|
|
55
62
|
it("should return same string when no email present", () => {
|
|
56
63
|
const input = "Just a normal message.";
|
|
57
|
-
const result = _cleanStringPII(input, TransportItemType.LOG
|
|
64
|
+
const result = _cleanStringPII(input, TransportItemType.LOG, [
|
|
65
|
+
redactors.email,
|
|
66
|
+
]);
|
|
58
67
|
|
|
59
68
|
expect(result).toBe(input);
|
|
60
69
|
expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
|
|
@@ -62,9 +71,109 @@ describe("_cleanStringPII", () => {
|
|
|
62
71
|
|
|
63
72
|
it("should return same string when decodeURIComponent fail", () => {
|
|
64
73
|
const input = "mailto%john.doe%4gmail.com";
|
|
65
|
-
const result = _cleanStringPII(input, TransportItemType.LOG
|
|
74
|
+
const result = _cleanStringPII(input, TransportItemType.LOG, [
|
|
75
|
+
redactors.email,
|
|
76
|
+
]);
|
|
66
77
|
|
|
67
78
|
expect(result).toBe(input);
|
|
68
79
|
expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
|
|
69
80
|
});
|
|
70
81
|
});
|
|
82
|
+
|
|
83
|
+
describe("_containsEncodedComponents", () => {
|
|
84
|
+
describe("should return true for properly URL encoded strings", () => {
|
|
85
|
+
it.each([
|
|
86
|
+
["hello%20world", "Space encoded as %20"],
|
|
87
|
+
["test%2Bvalue", "Plus sign encoded as %2B"],
|
|
88
|
+
["path%2Fto%2Ffile", "Forward slashes encoded"],
|
|
89
|
+
["user%40domain.com", "@ symbol encoded"],
|
|
90
|
+
["100%25%20off", "Percent and space encoded"],
|
|
91
|
+
["a%3Db%26c%3Dd", "Query parameters (a=b&c=d)"],
|
|
92
|
+
["caf%C3%A9", "UTF-8 encoded (café)"],
|
|
93
|
+
["price%3A%20%2410", "Colon, space, dollar ($10)"],
|
|
94
|
+
["%22quoted%22", "Double quotes encoded"],
|
|
95
|
+
["https%3A%2F%2Fexample.com", "Full URL encoded"],
|
|
96
|
+
["file%20name.txt", "filename encoded"],
|
|
97
|
+
["search%3Fq%3Dhello%20world", "Query string encoded"],
|
|
98
|
+
["%3C%3E%26%22%27", "HTML special chars encoded (<>&\"')"],
|
|
99
|
+
["%E2%9C%93", "UTF-8 checkmark (✓) encoded"],
|
|
100
|
+
["test%2b", "Lowercase hex"],
|
|
101
|
+
["%E4%B8%AD%E6%96%87", "Chinese characters encoded"],
|
|
102
|
+
])('should detect "%s" as URL encoded (%s)', (input, description) => {
|
|
103
|
+
expect(_containsEncodedComponents(input)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("should return false for non-URL encoded strings", () => {
|
|
108
|
+
it.each([
|
|
109
|
+
["test", "Simple ASCII string"],
|
|
110
|
+
["hello world", "Unencoded space"],
|
|
111
|
+
["user@domain.com", "Unencoded email"],
|
|
112
|
+
["simple123", "Alphanumeric only"],
|
|
113
|
+
["", "Empty string"],
|
|
114
|
+
["25%%", "Literal percent signs"],
|
|
115
|
+
["100% off", "Percent without hex digits"],
|
|
116
|
+
["test%2", "Incomplete percent encoding"],
|
|
117
|
+
["hello%ZZ", "Invalid hex digits"],
|
|
118
|
+
["test%2G", "Invalid hex digit G"],
|
|
119
|
+
["bad%encoding%here", "Percent without hex pairs"],
|
|
120
|
+
["hello%20world and more", "Partially encoded string"],
|
|
121
|
+
["hello%20world%21%20how%20are%20you", "Overly encoded string"],
|
|
122
|
+
["café", "Unicode characters (unencoded)"],
|
|
123
|
+
["hello+world", "Plus sign (form encoding style)"],
|
|
124
|
+
["test%", "Trailing percent"],
|
|
125
|
+
["hello%20world%", "Encoded content with trailing percent"],
|
|
126
|
+
["%", "Single percent"],
|
|
127
|
+
["%%", "Double percent"],
|
|
128
|
+
["normal text with % symbols", "Text with percent but no encoding"],
|
|
129
|
+
["price: $100%", "Currency with percent"],
|
|
130
|
+
["file.txt", "Simple filename"],
|
|
131
|
+
["path/to/file", "Unencoded path"],
|
|
132
|
+
["query?param=value", "Unencoded query string"],
|
|
133
|
+
["hello%world", "Percent without following hex"],
|
|
134
|
+
["test%1", "Single hex digit after percent"],
|
|
135
|
+
["hello%zz", "Non-hex characters after percent"],
|
|
136
|
+
])('should not detect "%s" as URL encoded (%s)', (input, description) => {
|
|
137
|
+
expect(_containsEncodedComponents(input)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("Error handling", () => {
|
|
142
|
+
it.each([
|
|
143
|
+
["%C0%80", "Overlong UTF-8 encoding (security concern)"],
|
|
144
|
+
["%ED%A0%80", "UTF-8 surrogate (invalid)"],
|
|
145
|
+
["%FF%FE", "Invalid UTF-8 sequence"],
|
|
146
|
+
["test%20%ZZ", "Mix of valid and invalid encoding"],
|
|
147
|
+
])('should handle "%s" (%s)', (input, description) => {
|
|
148
|
+
// These should not throw errors
|
|
149
|
+
expect(() => _containsEncodedComponents(input)).not.toThrow();
|
|
150
|
+
|
|
151
|
+
// Most of these should return false due to invalid sequences
|
|
152
|
+
// or security-related encoding issues
|
|
153
|
+
const result = _containsEncodedComponents(input);
|
|
154
|
+
expect(typeof result).toBe("boolean");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("real-world url examples", () => {
|
|
159
|
+
it.each([
|
|
160
|
+
[
|
|
161
|
+
"https%3A%2F%2Fgoogle.com%2Fsearch%3Fq%3Djavascript",
|
|
162
|
+
"Encoded Google search URL",
|
|
163
|
+
],
|
|
164
|
+
["The%20quick%20brown%20fox", "Sentence with spaces encoded"],
|
|
165
|
+
[
|
|
166
|
+
"/api/user?body=here%20are%20all%20my%20secrets",
|
|
167
|
+
"contextually encoded API path",
|
|
168
|
+
],
|
|
169
|
+
["redirect_uri=https%3A%2F%2Fapp.com%2Fcallback", "OAuth redirect URI"],
|
|
170
|
+
["data%3Atext%2Fplain%3Bbase64%2CSGVsbG8%3D", "Data URL encoded"],
|
|
171
|
+
])('real-world case: "%s" (%s)', (input, description) => {
|
|
172
|
+
const result = _containsEncodedComponents(input);
|
|
173
|
+
|
|
174
|
+
// Verify the function doesn't crash
|
|
175
|
+
expect(typeof result).toBe("boolean");
|
|
176
|
+
expect(result).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -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;
|