@ogcio/o11y-sdk-react 0.4.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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
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)
4
10
 
5
11
 
@@ -1,5 +1,12 @@
1
1
  import { TransportItemType } from "@grafana/faro-web-sdk";
2
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;
3
10
  /**
4
11
  * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
5
12
  *
@@ -4,13 +4,21 @@
4
4
  * @param {string} value - The string to inspect.
5
5
  * @returns {boolean} `true` if the string is encoded, `false` otherwise.
6
6
  */
7
- function _containsEncodedComponents(value) {
7
+ export function _containsEncodedComponents(value) {
8
8
  try {
9
- return decodeURI(value) !== decodeURIComponent(value);
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
+ }
10
17
  }
11
18
  catch {
12
19
  return false;
13
20
  }
21
+ return false;
14
22
  }
15
23
  /**
16
24
  * Cleans a string by redacting configured PIIs and emitting metrics for redacted values.
@@ -7,12 +7,24 @@ import { Redactor } from "./redactors/index.js";
7
7
  * @param {string} value - The string to inspect.
8
8
  * @returns {boolean} `true` if the string is encoded, `false` otherwise.
9
9
  */
10
- function _containsEncodedComponents(value: string) {
10
+ export function _containsEncodedComponents(value: string): boolean {
11
11
  try {
12
- return decodeURI(value) !== decodeURIComponent(value);
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
+ }
13
23
  } catch {
14
24
  return false;
15
25
  }
26
+
27
+ return false;
16
28
  }
17
29
 
18
30
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-react",
3
- "version": "0.4.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",
@@ -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 { _cleanStringPII } from "../../lib/internals/redaction/pii-detection";
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,9 +20,6 @@ vi.mock("@grafana/faro-web-sdk", async () => {
15
20
  };
16
21
  });
17
22
 
18
- import { faro } from "@grafana/faro-web-sdk";
19
- import { redactors } from "../../lib/internals/redaction/redactors";
20
-
21
23
  describe("_cleanStringPII", () => {
22
24
  beforeEach(() => {
23
25
  vi.clearAllMocks();
@@ -77,3 +79,101 @@ describe("_cleanStringPII", () => {
77
79
  expect(faro.api.pushMeasurement).not.toHaveBeenCalled();
78
80
  });
79
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
+ });