@ogcio/o11y-sdk-react 0.4.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/sdk-core/index.d.ts +2 -0
  2. package/dist/sdk-core/index.d.ts.map +1 -0
  3. package/dist/sdk-core/index.js +2 -0
  4. package/dist/sdk-core/index.js.map +1 -0
  5. package/dist/sdk-core/lib/index.d.ts +3 -0
  6. package/dist/sdk-core/lib/index.d.ts.map +1 -0
  7. package/dist/sdk-core/lib/index.js +3 -0
  8. package/dist/sdk-core/lib/index.js.map +1 -0
  9. package/dist/sdk-core/lib/redaction/basic-redactor.d.ts +8 -0
  10. package/dist/sdk-core/lib/redaction/basic-redactor.d.ts.map +1 -0
  11. package/dist/sdk-core/lib/redaction/basic-redactor.js +19 -0
  12. package/dist/sdk-core/lib/redaction/basic-redactor.js.map +1 -0
  13. package/dist/sdk-core/lib/redaction/email-redactor.d.ts +9 -0
  14. package/dist/sdk-core/lib/redaction/email-redactor.d.ts.map +1 -0
  15. package/dist/sdk-core/lib/redaction/email-redactor.js +18 -0
  16. package/dist/sdk-core/lib/redaction/email-redactor.js.map +1 -0
  17. package/dist/sdk-core/lib/redaction/index.d.ts +10 -0
  18. package/dist/sdk-core/lib/redaction/index.d.ts.map +1 -0
  19. package/dist/sdk-core/lib/redaction/index.js +5 -0
  20. package/dist/sdk-core/lib/redaction/index.js.map +1 -0
  21. package/dist/sdk-core/lib/redaction/ip-redactor.d.ts +10 -0
  22. package/dist/sdk-core/lib/redaction/ip-redactor.d.ts.map +1 -0
  23. package/dist/sdk-core/lib/redaction/ip-redactor.js +24 -0
  24. package/dist/sdk-core/lib/redaction/ip-redactor.js.map +1 -0
  25. package/dist/sdk-core/lib/redaction/ppsn-redactor.d.ts +9 -0
  26. package/dist/sdk-core/lib/redaction/ppsn-redactor.d.ts.map +1 -0
  27. package/dist/sdk-core/lib/redaction/ppsn-redactor.js +18 -0
  28. package/dist/sdk-core/lib/redaction/ppsn-redactor.js.map +1 -0
  29. package/dist/sdk-core/lib/utils/data-structures.d.ts +16 -0
  30. package/dist/sdk-core/lib/utils/data-structures.d.ts.map +1 -0
  31. package/dist/sdk-core/lib/utils/data-structures.js +58 -0
  32. package/dist/sdk-core/lib/utils/data-structures.js.map +1 -0
  33. package/dist/sdk-core/lib/utils/index.d.ts +3 -0
  34. package/dist/sdk-core/lib/utils/index.d.ts.map +1 -0
  35. package/dist/sdk-core/lib/utils/index.js +3 -0
  36. package/dist/sdk-core/lib/utils/index.js.map +1 -0
  37. package/dist/sdk-core/lib/utils/string-decoding.d.ts +8 -0
  38. package/dist/sdk-core/lib/utils/string-decoding.d.ts.map +1 -0
  39. package/dist/sdk-core/lib/utils/string-decoding.js +23 -0
  40. package/dist/sdk-core/lib/utils/string-decoding.js.map +1 -0
  41. package/dist/sdk-react/index.d.ts +7 -0
  42. package/dist/sdk-react/index.d.ts.map +1 -0
  43. package/dist/{index.js → sdk-react/index.js} +1 -0
  44. package/dist/sdk-react/index.js.map +1 -0
  45. package/dist/{lib → sdk-react/lib}/index.d.ts +12 -0
  46. package/dist/sdk-react/lib/index.d.ts.map +1 -0
  47. package/dist/sdk-react/lib/index.js +2 -0
  48. package/dist/sdk-react/lib/index.js.map +1 -0
  49. package/dist/{lib → sdk-react/lib}/instrumentation.faro.d.ts +1 -0
  50. package/dist/sdk-react/lib/instrumentation.faro.d.ts.map +1 -0
  51. package/dist/{lib → sdk-react/lib}/instrumentation.faro.js +7 -1
  52. package/dist/sdk-react/lib/instrumentation.faro.js.map +1 -0
  53. package/dist/sdk-react/lib/internals/hooks.d.ts +4 -0
  54. package/dist/sdk-react/lib/internals/hooks.d.ts.map +1 -0
  55. package/dist/sdk-react/lib/internals/hooks.js +8 -0
  56. package/dist/sdk-react/lib/internals/hooks.js.map +1 -0
  57. package/dist/sdk-react/lib/internals/redaction/redactors/email.d.ts +6 -0
  58. package/dist/sdk-react/lib/internals/redaction/redactors/email.d.ts.map +1 -0
  59. package/dist/sdk-react/lib/internals/redaction/redactors/email.js +22 -0
  60. package/dist/sdk-react/lib/internals/redaction/redactors/email.js.map +1 -0
  61. package/dist/sdk-react/lib/internals/redaction/redactors/index.d.ts +5 -0
  62. package/dist/sdk-react/lib/internals/redaction/redactors/index.d.ts.map +1 -0
  63. package/dist/sdk-react/lib/internals/redaction/redactors/index.js +9 -0
  64. package/dist/sdk-react/lib/internals/redaction/redactors/index.js.map +1 -0
  65. package/dist/sdk-react/lib/internals/redaction/redactors/ip.d.ts +6 -0
  66. package/dist/sdk-react/lib/internals/redaction/redactors/ip.d.ts.map +1 -0
  67. package/dist/sdk-react/lib/internals/redaction/redactors/ip.js +23 -0
  68. package/dist/sdk-react/lib/internals/redaction/redactors/ip.js.map +1 -0
  69. package/dist/sdk-react/lib/internals/redaction/redactors/ppsn.d.ts +8 -0
  70. package/dist/sdk-react/lib/internals/redaction/redactors/ppsn.d.ts.map +1 -0
  71. package/dist/sdk-react/lib/internals/redaction/redactors/ppsn.js +21 -0
  72. package/dist/sdk-react/lib/internals/redaction/redactors/ppsn.js.map +1 -0
  73. package/dist/{vitest.config.d.ts → sdk-react/vitest.config.d.ts} +1 -0
  74. package/dist/sdk-react/vitest.config.d.ts.map +1 -0
  75. package/dist/{vitest.config.js → sdk-react/vitest.config.js} +3 -2
  76. package/dist/sdk-react/vitest.config.js.map +1 -0
  77. package/package.json +19 -15
  78. package/CHANGELOG.md +0 -83
  79. package/dist/index.d.ts +0 -5
  80. package/dist/lib/index.js +0 -1
  81. package/dist/lib/internals/hooks.d.ts +0 -3
  82. package/dist/lib/internals/hooks.js +0 -34
  83. package/dist/lib/internals/redaction/pii-detection.d.ts +0 -26
  84. package/dist/lib/internals/redaction/pii-detection.js +0 -47
  85. package/dist/lib/internals/redaction/redactors/email.d.ts +0 -8
  86. package/dist/lib/internals/redaction/redactors/email.js +0 -55
  87. package/dist/lib/internals/redaction/redactors/index.d.ts +0 -4
  88. package/dist/lib/internals/redaction/redactors/index.js +0 -6
  89. package/dist/lib/internals/redaction/redactors/ip.d.ts +0 -10
  90. package/dist/lib/internals/redaction/redactors/ip.js +0 -61
  91. package/index.ts +0 -10
  92. package/lib/index.ts +0 -63
  93. package/lib/instrumentation.faro.ts +0 -102
  94. package/lib/internals/hooks.ts +0 -55
  95. package/lib/internals/redaction/pii-detection.ts +0 -64
  96. package/lib/internals/redaction/redactors/email.ts +0 -68
  97. package/lib/internals/redaction/redactors/index.ts +0 -12
  98. package/lib/internals/redaction/redactors/ip.ts +0 -78
  99. package/test/config.test.ts +0 -215
  100. package/test/lib/hooks.test.ts +0 -150
  101. package/test/lib/internals/redactors/email.test.ts +0 -81
  102. package/test/lib/internals/redactors/ip.test.ts +0 -86
  103. package/test/lib/pii-detection.test.ts +0 -179
  104. package/test/validation.test.ts +0 -65
  105. package/tsconfig.json +0 -14
  106. package/vitest.config.ts +0 -26
@@ -1,102 +0,0 @@
1
- import {
2
- ErrorsInstrumentation,
3
- getWebInstrumentations,
4
- initializeFaro,
5
- WebVitalsInstrumentation,
6
- type Faro,
7
- } from "@grafana/faro-react";
8
- import { TracingInstrumentation } from "@grafana/faro-web-tracing";
9
- import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
10
- import { W3CTraceContextPropagator } from "@opentelemetry/core";
11
- import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
12
- import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
13
- import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
14
- import type { FaroSDKConfig } from "./index.js";
15
- import { _beforeSend } from "./internals/hooks.js";
16
- import {
17
- RedactorKeys,
18
- redactors,
19
- } from "./internals/redaction/redactors/index.js";
20
-
21
- export default function buildFaroInstrumentation(
22
- config?: FaroSDKConfig,
23
- ): Faro | undefined {
24
- if (!config) {
25
- console.warn(
26
- "observability config not set. Skipping Faro OpenTelemetry instrumentation.",
27
- );
28
- return;
29
- }
30
-
31
- if (!config.collectorUrl) {
32
- console.warn(
33
- "collectorUrl not set. Skipping Faro OpenTelemetry instrumentation.",
34
- );
35
- return;
36
- }
37
-
38
- if (!isUrl(config.collectorUrl)) {
39
- console.error(
40
- "collectorUrl does not use a valid format. Skipping Faro OpenTelemetry instrumentation.",
41
- );
42
- return;
43
- }
44
-
45
- const redactorsChain = Object.entries(redactors)
46
- .filter(([key]) => {
47
- return config.detection?.[key as RedactorKeys] !== false;
48
- })
49
- .map(([_, value]) => value);
50
- try {
51
- diag.setLogger(
52
- new DiagConsoleLogger(),
53
- config.diagLogLevel
54
- ? DiagLogLevel[config.diagLogLevel]
55
- : DiagLogLevel.INFO,
56
- );
57
-
58
- const faro = initializeFaro({
59
- url: config.collectorUrl,
60
- app: {
61
- name: config.serviceName,
62
- },
63
- batching: {
64
- enabled: !(config.collectorMode === "single"),
65
- },
66
- beforeSend: _beforeSend(redactorsChain),
67
- instrumentations: [
68
- ...getWebInstrumentations({
69
- captureConsole: true,
70
- }),
71
- new ErrorsInstrumentation(),
72
- new WebVitalsInstrumentation(),
73
- new TracingInstrumentation({
74
- propagator: new W3CTraceContextPropagator(),
75
- instrumentations: [
76
- new DocumentLoadInstrumentation(),
77
- new FetchInstrumentation({
78
- ignoreUrls: [config.collectorUrl], // ignore collector to avoid fetch loop
79
- propagateTraceHeaderCorsUrls: config.corsTraceHeaders
80
- ? config.corsTraceHeaders.split(",").map((s) => new RegExp(s))
81
- : undefined,
82
- }),
83
- new UserInteractionInstrumentation(),
84
- ],
85
- }),
86
- ],
87
- });
88
- console.log("Faro OpenTelemetry instrumentation started successfully.");
89
- return faro;
90
- } catch (error) {
91
- console.error("Error starting Faro OpenTelemetry instrumentation:", error);
92
- }
93
- }
94
-
95
- function isUrl(url: string): boolean {
96
- try {
97
- new URL(url);
98
- return true;
99
- } catch (_) {
100
- return false;
101
- }
102
- }
@@ -1,55 +0,0 @@
1
- import {
2
- APIEvent,
3
- TransportItem,
4
- TransportItemType,
5
- } from "@grafana/faro-web-sdk";
6
- import { _cleanStringPII } from "./redaction/pii-detection.js";
7
- import { Redactor } from "./redaction/redactors/index.js";
8
-
9
- function _cleanObjectPII<
10
- T extends string | Record<string, unknown> | unknown[] | unknown,
11
- >(data: T, type: TransportItemType, redactors: Redactor[]): void {
12
- if (!data) {
13
- return;
14
- }
15
-
16
- if (Array.isArray(data)) {
17
- data.forEach((item, index) => {
18
- if (typeof item === "string") {
19
- data[index] = _cleanStringPII(data[index], type, redactors);
20
- return;
21
- }
22
-
23
- if (typeof item === "object") {
24
- _cleanObjectPII<T>(item, type, redactors);
25
- }
26
- });
27
- }
28
-
29
- if (typeof data === "object") {
30
- for (const key in data) {
31
- if (typeof data[key] === "string") {
32
- data[key] = _cleanStringPII(
33
- data[key],
34
- type,
35
- redactors,
36
- ) as (typeof data)[typeof key];
37
- }
38
-
39
- if (typeof data[key] === "object") {
40
- _cleanObjectPII(data[key], type, redactors);
41
- }
42
- }
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
- };
@@ -1,64 +0,0 @@
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,68 +0,0 @@
1
- import { faro } from "@grafana/faro-web-sdk";
2
-
3
- const EMAIL_REGEX = /[\p{L}\p{N}._%+-]+@((?:[\p{L}\p{N}-]+\.)+[\p{L}]{2,})/giu;
4
-
5
- /**
6
- * Redacts all email addresses in the input string and collects metadata.
7
- *
8
- * @param {string} value The input string potentially containing email addresses.
9
- * @returns {{
10
- * redacted: string,
11
- * count: number,
12
- * domains: Record<string, number>
13
- * }}
14
- *
15
- * An object containing:
16
- * - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
17
- * - `count`: total number of email addresses redacted
18
- * - `domains`: a map of domain names to the number of times they were redacted
19
- */
20
- function _redactEmails(value: string): {
21
- redacted: string;
22
- count: number;
23
- domains: Record<string, number>;
24
- } {
25
- let count = 0;
26
- const domains: Record<string, number> = {};
27
-
28
- const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
29
- count++;
30
- domains[domain] = (domains[domain] || 0) + 1;
31
- return "[REDACTED EMAIL]";
32
- });
33
-
34
- return { redacted, count, domains };
35
- }
36
-
37
- /**
38
- * Redacts provided input and collects metadata metrics about redacted email domains,
39
- * data source and kind.
40
- *
41
- * @param {string} value The input string potentially containing email addresses.
42
- * @returns {string} the redacted value
43
- */
44
- export const emailRedactor = (value: string, source: string, kind: string) => {
45
- const { redacted, count, domains } = _redactEmails(value);
46
-
47
- if (count > 0) {
48
- for (const [domain, domainCount] of Object.entries(domains)) {
49
- faro.api.pushMeasurement(
50
- {
51
- type: "faro_o11y_pii_redaction",
52
- values: {
53
- redacted: domainCount,
54
- },
55
- },
56
- {
57
- context: {
58
- pii_type: "email",
59
- redaction_source: source,
60
- pii_email_domain: domain,
61
- pii_format: kind,
62
- },
63
- },
64
- );
65
- }
66
- }
67
- return redacted;
68
- };
@@ -1,12 +0,0 @@
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
- };
@@ -1,78 +0,0 @@
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
- };
@@ -1,215 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import buildFaroInstrumentation from "../lib/instrumentation.faro";
3
- import { initializeFaro } from "@grafana/faro-react";
4
- import { _beforeSend } from "../lib/internals/hooks";
5
- import { redactors } from "../lib/internals/redaction/redactors";
6
- import * as detectionTools from "../lib/internals/redaction/pii-detection";
7
-
8
- // Mock dependencies
9
- vi.mock("@grafana/faro-react", async () => {
10
- const actual = await vi.importActual<any>("@grafana/faro-react");
11
- return {
12
- ...actual,
13
- initializeFaro: vi.fn(() => ({ fakeFaro: true })),
14
- getWebInstrumentations: vi.fn(() => ["webInstrumentation"]),
15
- ErrorsInstrumentation: vi.fn(() => "errorsInstrumentation"),
16
- WebVitalsInstrumentation: vi.fn(() => "webVitalsInstrumentation"),
17
- };
18
- });
19
-
20
- vi.mock("@grafana/faro-web-tracing", () => ({
21
- TracingInstrumentation: vi.fn(() => "tracingInstrumentation"),
22
- }));
23
-
24
- vi.mock("@opentelemetry/core", () => ({
25
- W3CTraceContextPropagator: vi.fn(() => "tracePropagator"),
26
- }));
27
-
28
- vi.mock("@opentelemetry/instrumentation-document-load", () => ({
29
- DocumentLoadInstrumentation: vi.fn(() => "docLoadInstrumentation"),
30
- }));
31
-
32
- vi.mock("@opentelemetry/instrumentation-fetch", () => ({
33
- FetchInstrumentation: vi.fn((opts) => ({
34
- ...opts,
35
- name: "fetchInstrumentation",
36
- })),
37
- }));
38
-
39
- vi.mock("@opentelemetry/instrumentation-user-interaction", () => ({
40
- UserInteractionInstrumentation: vi.fn(() => "userInteractionInstrumentation"),
41
- }));
42
-
43
- vi.mock("./internals/hooks", () => ({
44
- _beforeSend: vi.fn(() => "hooked"),
45
- }));
46
-
47
- describe("buildFaroInstrumentation", () => {
48
- const validConfig = {
49
- collectorUrl: "https://valid-url.com",
50
- serviceName: "test-app",
51
- detection: {
52
- email: true,
53
- },
54
- };
55
-
56
- beforeEach(() => {
57
- vi.clearAllMocks();
58
- console.warn = vi.fn();
59
- console.error = vi.fn();
60
- console.log = vi.fn();
61
- });
62
-
63
- it("returns undefined and warns if config is not provided", () => {
64
- const result = buildFaroInstrumentation();
65
- expect(console.warn).toHaveBeenCalledWith(
66
- "observability config not set. Skipping Faro OpenTelemetry instrumentation.",
67
- );
68
- expect(result).toBeUndefined();
69
- });
70
-
71
- it("returns undefined and warns if collectorUrl is missing", () => {
72
- const result = buildFaroInstrumentation({
73
- ...validConfig,
74
- collectorUrl: undefined!,
75
- });
76
- expect(console.warn).toHaveBeenCalledWith(
77
- "collectorUrl not set. Skipping Faro OpenTelemetry instrumentation.",
78
- );
79
- expect(result).toBeUndefined();
80
- });
81
-
82
- it("returns undefined and errors if collectorUrl is invalid", () => {
83
- const result = buildFaroInstrumentation({
84
- ...validConfig,
85
- collectorUrl: "not-a-url",
86
- });
87
- expect(console.error).toHaveBeenCalledWith(
88
- "collectorUrl does not use a valid format. Skipping Faro OpenTelemetry instrumentation.",
89
- );
90
- expect(result).toBeUndefined();
91
- });
92
-
93
- it("defaults all detection to true if not provided", () => {
94
- const config = {
95
- ...validConfig,
96
- detection: {},
97
- };
98
- buildFaroInstrumentation(config);
99
- expect(initializeFaro).toHaveBeenCalledOnce();
100
- expect(initializeFaro.mock.calls[0][0].beforeSend.toString()).toEqual(
101
- _beforeSend(Object.values(redactors)).toString(),
102
- );
103
- });
104
-
105
- it("sets beforeSend if detection.email is true", () => {
106
- buildFaroInstrumentation({ ...validConfig, detection: { email: true } });
107
- expect(initializeFaro).toHaveBeenCalledOnce();
108
- expect(initializeFaro.mock.calls[0][0].beforeSend.toString()).toEqual(
109
- _beforeSend(Object.values(redactors)).toString(),
110
- );
111
- });
112
-
113
- it("does not set beforeSend if detection.email is false", () => {
114
- buildFaroInstrumentation({ ...validConfig, detection: { email: false } });
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(),
122
- );
123
- });
124
-
125
- it("disables batching when collectorMode is 'single'", () => {
126
- buildFaroInstrumentation({ ...validConfig, collectorMode: "single" });
127
- expect(initializeFaro).toHaveBeenCalledWith(
128
- expect.objectContaining({
129
- batching: {
130
- enabled: false,
131
- },
132
- }),
133
- );
134
- });
135
-
136
- it("sets diagLogLevel if provided", async () => {
137
- const { DiagLogLevel } = await import("@opentelemetry/api");
138
- const setLoggerSpy = vi
139
- .spyOn(await import("@opentelemetry/api"), "diag", "get")
140
- .mockReturnValue({
141
- setLogger: vi.fn(),
142
- });
143
-
144
- buildFaroInstrumentation({ ...validConfig, diagLogLevel: "DEBUG" });
145
-
146
- expect(setLoggerSpy().setLogger).toHaveBeenCalledWith(
147
- expect.anything(),
148
- DiagLogLevel.DEBUG,
149
- );
150
- });
151
-
152
- it("handles initialization failure and logs error", () => {
153
- (initializeFaro as any).mockImplementationOnce(() => {
154
- throw new Error("init failed");
155
- });
156
-
157
- const result = buildFaroInstrumentation(validConfig);
158
- expect(console.error).toHaveBeenCalledWith(
159
- "Error starting Faro OpenTelemetry instrumentation:",
160
- expect.any(Error),
161
- );
162
- expect(result).toBeUndefined();
163
- });
164
-
165
- it("returns the faro object on successful initialization", () => {
166
- const result = buildFaroInstrumentation(validConfig);
167
- expect(console.log).toHaveBeenCalledWith(
168
- "Faro OpenTelemetry instrumentation started successfully.",
169
- );
170
- expect(result).toEqual({ fakeFaro: true });
171
- });
172
-
173
- it("skips detection if all redactors are set to false", () => {
174
- const config = {
175
- ...validConfig,
176
- detection: Object.fromEntries(
177
- Object.keys(redactors).map((key) => [key, false]),
178
- ),
179
- };
180
-
181
- buildFaroInstrumentation(config);
182
-
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();
195
- });
196
-
197
- it("handles comma-separated corsTraceHeaders", async () => {
198
- buildFaroInstrumentation({
199
- ...validConfig,
200
- corsTraceHeaders: "https://site1.com,https://site2.com",
201
- });
202
-
203
- const { FetchInstrumentation } = await import(
204
- "@opentelemetry/instrumentation-fetch"
205
- );
206
- expect(FetchInstrumentation).toHaveBeenCalledWith(
207
- expect.objectContaining({
208
- propagateTraceHeaderCorsUrls: [
209
- new RegExp("https://site1.com"),
210
- new RegExp("https://site2.com"),
211
- ],
212
- }),
213
- );
214
- });
215
- });