@ogcio/o11y-sdk-node 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 +14 -0
- package/dist/lib/config-manager.d.ts +1 -1
- package/dist/lib/config-manager.js +1 -4
- package/dist/lib/exporter/pii-exporter-decorator.d.ts +1 -1
- package/dist/lib/exporter/pii-exporter-decorator.js +10 -10
- package/dist/lib/internals/redaction/pii-detection.d.ts +11 -9
- package/dist/lib/internals/redaction/pii-detection.js +17 -24
- package/dist/lib/internals/redaction/redactors/ip.js +2 -2
- package/dist/lib/traces.js +1 -1
- package/dist/package.json +1 -1
- package/dist/vitest.config.js +1 -1
- package/lib/config-manager.ts +2 -6
- package/lib/exporter/pii-exporter-decorator.ts +15 -12
- package/lib/internals/redaction/pii-detection.ts +27 -40
- package/lib/internals/redaction/redactors/ip.ts +2 -2
- package/lib/traces.ts +2 -2
- package/package.json +1 -1
- package/test/config-manager.test.ts +2 -2
- package/test/integration/README.md +59 -11
- package/test/integration/docker-utils.sh +214 -0
- package/test/integration/main.sh +52 -0
- package/test/integration/teardown.sh +7 -0
- package/test/integration/{http-tracing.integration.test.ts → test_fastify-o11y-pii-enabled/http-tracing.integration.test.ts} +1 -1
- package/test/integration/{pii.integration.test.ts → test_fastify-o11y-pii-enabled/pii.integration.test.ts} +1 -1
- package/test/integration/test_fastify-o11y-pii-enabled/run.sh +42 -0
- package/test/integration/test_without-o11y/run.sh +30 -0
- package/test/integration/test_without-o11y/verify-status.integration.test.ts +32 -0
- package/test/internals/pii-detection.test.ts +142 -20
- package/test/internals/redactors/ip.test.ts +4 -0
- package/test/traces/active-span.test.ts +2 -4
- package/test/traces/with-span.test.ts +16 -0
- package/vitest.config.ts +1 -1
- package/test/integration/run.sh +0 -88
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
: '
|
|
4
|
+
Build a new docker image of a given Dockerfile, if already exists skip build.
|
|
5
|
+
|
|
6
|
+
@param container_name docker container name
|
|
7
|
+
|
|
8
|
+
@return int error code
|
|
9
|
+
0. success
|
|
10
|
+
1. validation error
|
|
11
|
+
'
|
|
12
|
+
build_image() {
|
|
13
|
+
if [ "$#" -lt 3 ] || [ "$#" -gt 4 ]; then
|
|
14
|
+
echo "Error: build_image expected 3 or 4 parameters, got $#." >&2
|
|
15
|
+
echo "Usage: build_image <image_name> <dockerfile_path> <context_path> [max_age_minutes]" >&2
|
|
16
|
+
return 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
local image_name="$1"
|
|
21
|
+
local dockerfile_path="$2"
|
|
22
|
+
local context_path="$3"
|
|
23
|
+
local max_age_minutes="${4:-30}" # default threshold = 30 minutes
|
|
24
|
+
|
|
25
|
+
if docker image inspect "$image_name" > /dev/null 2>&1; then
|
|
26
|
+
echo "Image $image_name already exists. Checking age..."
|
|
27
|
+
|
|
28
|
+
# Extract the Created timestamp from docker inspect (ISO 8601 format)
|
|
29
|
+
local created_at
|
|
30
|
+
created_at=$(docker image inspect --format '{{.Created}}' "$image_name")
|
|
31
|
+
if [ -z "$created_at" ]; then
|
|
32
|
+
echo "Warning: Could not determine creation date. Proceeding without rebuild check." >&2
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Convert created_at to epoch seconds
|
|
37
|
+
local created_epoch
|
|
38
|
+
created_epoch=$(date -d "$created_at" +%s 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
# Current time in epoch seconds
|
|
41
|
+
local now_epoch
|
|
42
|
+
now_epoch=$(date +%s)
|
|
43
|
+
|
|
44
|
+
# Calculate age in minutes
|
|
45
|
+
local age_minutes=$(( (now_epoch - created_epoch) / 60 ))
|
|
46
|
+
|
|
47
|
+
echo "Image $image_name is $age_minutes minutes old (threshold: $max_age_minutes minutes)."
|
|
48
|
+
|
|
49
|
+
if [ "$age_minutes" -ge "$max_age_minutes" ]; then
|
|
50
|
+
echo "Image is too old. Rebuilding..."
|
|
51
|
+
docker build -t $image_name -f $dockerfile_path $context_path
|
|
52
|
+
return $?
|
|
53
|
+
else
|
|
54
|
+
echo "Image is recent. Skipping rebuild."
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
else
|
|
58
|
+
echo "Image $image_name not found. Building..."
|
|
59
|
+
docker build -t "$image_name" -f "$dockerfile_path" "$context_path"
|
|
60
|
+
return $?
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
: '
|
|
65
|
+
Stop and remove a docker container for the given input container name.
|
|
66
|
+
|
|
67
|
+
@param container_name docker container name
|
|
68
|
+
|
|
69
|
+
@return int error code
|
|
70
|
+
0. success
|
|
71
|
+
1. validation error
|
|
72
|
+
'
|
|
73
|
+
container_stop_and_rm() {
|
|
74
|
+
local container_name="$1"
|
|
75
|
+
|
|
76
|
+
if [[ -z "$container_name" ]]; then
|
|
77
|
+
echo "Error: container_name must have a value."
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
docker container stop $container_name
|
|
82
|
+
docker container rm -f $container_name
|
|
83
|
+
|
|
84
|
+
return $?
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
: '
|
|
88
|
+
Run a new alloy container and check if the status is running.
|
|
89
|
+
|
|
90
|
+
@param root_path volume root path for alloy file config
|
|
91
|
+
@param network_name docker network name
|
|
92
|
+
@param container_name docker container name
|
|
93
|
+
|
|
94
|
+
@optional max_retries
|
|
95
|
+
@default 10
|
|
96
|
+
|
|
97
|
+
@return int error code
|
|
98
|
+
0. success
|
|
99
|
+
1. validation error
|
|
100
|
+
2. execution failed
|
|
101
|
+
'
|
|
102
|
+
run_and_check_alloy() {
|
|
103
|
+
|
|
104
|
+
if [ "$#" -lt 3 ]; then
|
|
105
|
+
echo "Error: run_and_check_alloy expected 3 parameters, got $#." >&2
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
local root_path="$1"
|
|
110
|
+
local network_name="$2"
|
|
111
|
+
local container_name="$3"
|
|
112
|
+
local max_retries="${4:-10}"
|
|
113
|
+
local counter_retries=0
|
|
114
|
+
|
|
115
|
+
if [[ -z "$root_path" || -z "$network_name" || -z "$container_name" ]]; then
|
|
116
|
+
echo "Error: root_path, network_name, and container_name must all have values."
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if ! [[ "$max_retries" =~ ^[0-9]+$ ]]; then
|
|
121
|
+
echo "Error: max_retries parameter must be a number." >&2
|
|
122
|
+
return 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
docker run -d \
|
|
126
|
+
-v "$root_path/alloy/integration-test.alloy:/etc/alloy/config.alloy:ro" \
|
|
127
|
+
--network $network_name \
|
|
128
|
+
--name $container_name \
|
|
129
|
+
-p 4317:4317 \
|
|
130
|
+
-p 4318:4318 \
|
|
131
|
+
grafana/alloy:v1.9.2 \
|
|
132
|
+
run --server.http.listen-addr=0.0.0.0:12345 --stability.level=experimental /etc/alloy/config.alloy
|
|
133
|
+
|
|
134
|
+
echo "$container_name container status"
|
|
135
|
+
until [ "$(docker inspect -f {{.State.Running}} $container_name)" = true ]; do
|
|
136
|
+
sleep 2
|
|
137
|
+
docker inspect -f {{.State.Running}} $container_name
|
|
138
|
+
counter_retries=$((counter_retries + 1))
|
|
139
|
+
if [ $counter_retries -ge $max_retries ]; then
|
|
140
|
+
echo "Exceeded maximum retries. Exiting."
|
|
141
|
+
return 2
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
return 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
: '
|
|
149
|
+
Run a new fastify microservice container and check if the status is running.
|
|
150
|
+
|
|
151
|
+
@param network_name docker network name
|
|
152
|
+
@param collector_url otel collector grpc endpoint
|
|
153
|
+
@param container_name docker container name
|
|
154
|
+
@param build_id current pipeline build id
|
|
155
|
+
|
|
156
|
+
@optional max_retries
|
|
157
|
+
@default 10
|
|
158
|
+
|
|
159
|
+
@return int error code
|
|
160
|
+
0. success
|
|
161
|
+
1. validation error
|
|
162
|
+
2. execution failed
|
|
163
|
+
'
|
|
164
|
+
run_and_check_fastify() {
|
|
165
|
+
|
|
166
|
+
if [ "$#" -lt 4 ]; then
|
|
167
|
+
echo "Error: run_and_check_alloy expected 3 parameters, got $#." >&2
|
|
168
|
+
return 1
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
local network_name="$1"
|
|
172
|
+
local collector_url="$2"
|
|
173
|
+
local container_name="$3"
|
|
174
|
+
local build_id="$4"
|
|
175
|
+
local max_retries="${5:-10}"
|
|
176
|
+
local counter_retries=0
|
|
177
|
+
|
|
178
|
+
if [[ -z "$build_id" || -z "$network_name" || -z "$container_name" ]]; then
|
|
179
|
+
echo "Error: build_id, network_name, and container_name must all have values."
|
|
180
|
+
return 1
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if ! [[ "$max_retries" =~ ^[0-9]+$ ]]; then
|
|
184
|
+
echo "Error: max_retries parameter must be a number." >&2
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
docker run --detach \
|
|
189
|
+
--network $network_name \
|
|
190
|
+
--name $container_name \
|
|
191
|
+
-e DB_DISABLED="true" \
|
|
192
|
+
-e SERVER_HOST="0.0.0.0" \
|
|
193
|
+
-e OTEL_COLLECTOR_URL=$collector_url \
|
|
194
|
+
--health-cmd="curl -f http://${container_name}:9091/api/health > /dev/null || exit 1" \
|
|
195
|
+
--health-start-period=1s \
|
|
196
|
+
--health-retries=10 \
|
|
197
|
+
--health-interval=1s \
|
|
198
|
+
-p 9091:9091 \
|
|
199
|
+
$container_name:$build_id
|
|
200
|
+
|
|
201
|
+
counter_retries=0
|
|
202
|
+
echo "$container_name container status"
|
|
203
|
+
until [ "$(docker inspect -f {{.State.Health.Status}} $container_name)" = "healthy" ]; do
|
|
204
|
+
sleep 1
|
|
205
|
+
docker inspect -f {{.State.Health.Status}} $container_name
|
|
206
|
+
counter_retries=$((counter_retries + 1))
|
|
207
|
+
if [ $counter_retries -ge $max_retries ]; then
|
|
208
|
+
echo "Exceeded maximum retries. Exiting."
|
|
209
|
+
return 2
|
|
210
|
+
fi
|
|
211
|
+
done
|
|
212
|
+
|
|
213
|
+
return 0
|
|
214
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
set -o pipefail
|
|
5
|
+
|
|
6
|
+
if [ $# -lt 2 ]; then
|
|
7
|
+
echo "Usage: $0 <build-id> <root-path>"
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
BUILD_ID=$1
|
|
12
|
+
ROOT_PATH=$2
|
|
13
|
+
|
|
14
|
+
NETWORK_NAME="${BUILD_ID}_testnetwork"
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
|
|
17
|
+
docker network create $NETWORK_NAME
|
|
18
|
+
|
|
19
|
+
TEST_SCRIPTS=( $(find . -type f -path "./packages/sdk-node/test/integration/test*/run.sh" | sort) )
|
|
20
|
+
|
|
21
|
+
FAILURES=0
|
|
22
|
+
|
|
23
|
+
for test_script in "${TEST_SCRIPTS[@]}"; do
|
|
24
|
+
TEST_DIR=$(dirname "$test_script")
|
|
25
|
+
TEST_NAME=$(basename "$TEST_DIR")
|
|
26
|
+
|
|
27
|
+
echo "Running test: $TEST_NAME ($test_script)"
|
|
28
|
+
|
|
29
|
+
# Run the test
|
|
30
|
+
bash "$test_script" "$BUILD_ID" "$ROOT_PATH" "$NETWORK_NAME"
|
|
31
|
+
EXIT_CODE=$?
|
|
32
|
+
|
|
33
|
+
if [ $EXIT_CODE -eq 0 ]; then
|
|
34
|
+
echo "$test_script completed"
|
|
35
|
+
else
|
|
36
|
+
echo "❌ $test_script failed with exit code $EXIT_CODE"
|
|
37
|
+
FAILURES=$((FAILURES + 1))
|
|
38
|
+
fi
|
|
39
|
+
echo "------------------------------"
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# Remove network and images from docker host
|
|
43
|
+
bash "$SCRIPT_DIR/teardown.sh" "$BUILD_ID" "$NETWORK_NAME"
|
|
44
|
+
|
|
45
|
+
# Final summary
|
|
46
|
+
if [ $FAILURES -eq 0 ]; then
|
|
47
|
+
echo "All tests ready!"
|
|
48
|
+
exit 0
|
|
49
|
+
else
|
|
50
|
+
echo "$FAILURES test(s) failed."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# @docs: packages/sdk-node/test/integration/README.md#test_fastify-o11y-pii-enabled
|
|
3
|
+
BUILD_ID=$1
|
|
4
|
+
ROOT_PATH=$2
|
|
5
|
+
NETWORK_NAME=$3
|
|
6
|
+
|
|
7
|
+
ALLOY_CONTAINER_NAME="integrationalloy"
|
|
8
|
+
NODE_CONTAINER_NAME="${BUILD_ID}_fastify_app"
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
LOG_FILE="$SCRIPT_DIR/logs.txt"
|
|
11
|
+
ERROR_CODE=0
|
|
12
|
+
|
|
13
|
+
source "$SCRIPT_DIR/../docker-utils.sh"
|
|
14
|
+
|
|
15
|
+
build_image "${NODE_CONTAINER_NAME}:${BUILD_ID}" "$ROOT_PATH/examples/fastify/Dockerfile" "$ROOT_PATH/"
|
|
16
|
+
|
|
17
|
+
run_and_check_alloy $ROOT_PATH $NETWORK_NAME $ALLOY_CONTAINER_NAME
|
|
18
|
+
ERROR_CODE=$?
|
|
19
|
+
|
|
20
|
+
if [[ $ERROR_CODE -eq 0 ]]; then
|
|
21
|
+
run_and_check_fastify $NETWORK_NAME "http://integrationalloy:4317" $NODE_CONTAINER_NAME $BUILD_ID
|
|
22
|
+
ERROR_CODE=$?
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ $ERROR_CODE -eq 0 ]]; then
|
|
26
|
+
sleep 2
|
|
27
|
+
curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
|
|
28
|
+
sleep 2
|
|
29
|
+
curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# sleep N seconds to await instrumentation flow send and receiving signals
|
|
33
|
+
sleep 1
|
|
34
|
+
|
|
35
|
+
# Copy logs from container to file
|
|
36
|
+
docker container logs $ALLOY_CONTAINER_NAME >&$LOG_FILE
|
|
37
|
+
echo "log file at $LOG_FILE"
|
|
38
|
+
|
|
39
|
+
container_stop_and_rm $ALLOY_CONTAINER_NAME
|
|
40
|
+
container_stop_and_rm $NODE_CONTAINER_NAME
|
|
41
|
+
|
|
42
|
+
exit $ERROR_CODE
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# @docs: packages/sdk-node/test/integration/README.md#test_without-o11y
|
|
3
|
+
BUILD_ID=$1
|
|
4
|
+
ROOT_PATH=$2
|
|
5
|
+
NETWORK_NAME=$3
|
|
6
|
+
|
|
7
|
+
NODE_CONTAINER_NAME="${BUILD_ID}_fastify_app"
|
|
8
|
+
ERROR_CODE=0
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
LOG_FILE="$SCRIPT_DIR/logs.txt"
|
|
11
|
+
|
|
12
|
+
source "$SCRIPT_DIR/../docker-utils.sh"
|
|
13
|
+
|
|
14
|
+
build_image "${NODE_CONTAINER_NAME}:${BUILD_ID}" "$ROOT_PATH/examples/fastify/Dockerfile" "$ROOT_PATH/"
|
|
15
|
+
|
|
16
|
+
run_and_check_fastify $NETWORK_NAME "" $NODE_CONTAINER_NAME $BUILD_ID
|
|
17
|
+
ERROR_CODE=$?
|
|
18
|
+
|
|
19
|
+
if [[ $ERROR_CODE -eq 0 ]]; then
|
|
20
|
+
curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
|
|
21
|
+
sleep 1
|
|
22
|
+
curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
docker container logs $NODE_CONTAINER_NAME >&$LOG_FILE
|
|
26
|
+
echo "log file at $LOG_FILE"
|
|
27
|
+
|
|
28
|
+
container_stop_and_rm $NODE_CONTAINER_NAME
|
|
29
|
+
|
|
30
|
+
exit $ERROR_CODE
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
describe("Log validation for /api/dummy with instrumentation disabled", async () => {
|
|
6
|
+
const log = await readFile(join(__dirname, "logs.txt"), "utf-8");
|
|
7
|
+
|
|
8
|
+
it("should contain message that collector URL is not set", () => {
|
|
9
|
+
expect(log).toContain(
|
|
10
|
+
"collectorUrl not set. Skipping NodeJS OpenTelemetry instrumentation.",
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should have all /api/dummy responses with status code 200", () => {
|
|
15
|
+
// Extract all request IDs for /api/dummy requests
|
|
16
|
+
const dummyRequestIds = [
|
|
17
|
+
...log.matchAll(/"reqId":"(.*?)","req":\{[^}]*"url":"\/api\/dummy"/g),
|
|
18
|
+
].map((match) => match[1]);
|
|
19
|
+
|
|
20
|
+
expect(dummyRequestIds.length).toBeGreaterThan(0);
|
|
21
|
+
|
|
22
|
+
// For each /api/dummy request ID, verify that its response status is 200
|
|
23
|
+
const non200Responses = dummyRequestIds.filter((reqId) => {
|
|
24
|
+
const responseMatch = log.match(
|
|
25
|
+
new RegExp(`"reqId":"${reqId}".*"statusCode":(\\d+)`),
|
|
26
|
+
);
|
|
27
|
+
return responseMatch && Number(responseMatch[1]) !== 200;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(non200Responses).toEqual([]); // No non-200 responses allowed
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
_cleanStringPII,
|
|
4
|
-
|
|
4
|
+
_containsEncodedComponents,
|
|
5
|
+
_recursiveObjectClean,
|
|
5
6
|
} from "../../lib/internals/redaction/pii-detection.js";
|
|
6
7
|
import * as sharedMetrics from "../../lib/internals/shared-metrics.js";
|
|
7
8
|
import { emailRedactor } from "../../lib/internals/redaction/redactors/email";
|
|
9
|
+
import { ipRedactor } from "../../lib/internals/redaction/redactors/ip";
|
|
8
10
|
|
|
9
11
|
describe("PII Detection Utils", () => {
|
|
10
12
|
const mockMetricAdd = vi.fn();
|
|
@@ -32,7 +34,7 @@ describe("PII Detection Utils", () => {
|
|
|
32
34
|
);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
it("redacts
|
|
37
|
+
it("redacts email in URL-encoded string", () => {
|
|
36
38
|
const input = "user%40gmail.com";
|
|
37
39
|
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
38
40
|
|
|
@@ -46,6 +48,21 @@ describe("PII Detection Utils", () => {
|
|
|
46
48
|
);
|
|
47
49
|
});
|
|
48
50
|
|
|
51
|
+
it("redacts ip in URL-encoded string", () => {
|
|
52
|
+
const input = "%20127.0.0.1";
|
|
53
|
+
const output = _cleanStringPII(input, "log", [ipRedactor]);
|
|
54
|
+
|
|
55
|
+
expect(output).toBe(" [REDACTED IPV4]");
|
|
56
|
+
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
57
|
+
1,
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
pii_format: "url",
|
|
60
|
+
pii_type: "IPv4",
|
|
61
|
+
redaction_source: "log",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
49
66
|
it("handles strings without PII unchanged", () => {
|
|
50
67
|
const input = "hello world";
|
|
51
68
|
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
@@ -54,16 +71,10 @@ describe("PII Detection Utils", () => {
|
|
|
54
71
|
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
55
72
|
});
|
|
56
73
|
|
|
57
|
-
it("handles array of strings", () => {
|
|
58
|
-
const input = ["one@gmail.com", "two@example.com"];
|
|
59
|
-
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
60
|
-
|
|
61
|
-
expect(output).toEqual(["[REDACTED EMAIL]", "[REDACTED EMAIL]"]);
|
|
62
|
-
expect(mockMetricAdd).toHaveBeenCalledTimes(2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
74
|
it("ignores non-string input", () => {
|
|
75
|
+
// @ts-expect-error
|
|
66
76
|
expect(_cleanStringPII(1234, "trace", [emailRedactor])).toBe(1234);
|
|
77
|
+
// @ts-expect-error
|
|
67
78
|
expect(_cleanStringPII(true, "trace", [emailRedactor])).toBe(true);
|
|
68
79
|
expect(
|
|
69
80
|
_cleanStringPII(undefined, "trace", [emailRedactor]),
|
|
@@ -72,12 +83,22 @@ describe("PII Detection Utils", () => {
|
|
|
72
83
|
});
|
|
73
84
|
});
|
|
74
85
|
|
|
75
|
-
describe("
|
|
86
|
+
describe("_recursiveObjectClean", () => {
|
|
76
87
|
it("cleans string PII", () => {
|
|
77
|
-
const result =
|
|
88
|
+
const result = _recursiveObjectClean("demo@abc.com", "log", [
|
|
89
|
+
emailRedactor,
|
|
90
|
+
]);
|
|
78
91
|
expect(result).toBe("[REDACTED EMAIL]");
|
|
79
92
|
});
|
|
80
93
|
|
|
94
|
+
it("cleans array of strings", () => {
|
|
95
|
+
const input = ["one@gmail.com", "two@example.com"];
|
|
96
|
+
const output = _recursiveObjectClean(input, "log", [emailRedactor]);
|
|
97
|
+
|
|
98
|
+
expect(output).toEqual(["[REDACTED EMAIL]", "[REDACTED EMAIL]"]);
|
|
99
|
+
expect(mockMetricAdd).toHaveBeenCalledTimes(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
81
102
|
it("cleans deeply nested object", () => {
|
|
82
103
|
const input = {
|
|
83
104
|
user: {
|
|
@@ -89,7 +110,7 @@ describe("PII Detection Utils", () => {
|
|
|
89
110
|
status: "active",
|
|
90
111
|
};
|
|
91
112
|
|
|
92
|
-
const result =
|
|
113
|
+
const result = _recursiveObjectClean(input, "log", [emailRedactor]);
|
|
93
114
|
|
|
94
115
|
expect(result).toEqual({
|
|
95
116
|
user: {
|
|
@@ -105,7 +126,7 @@ describe("PII Detection Utils", () => {
|
|
|
105
126
|
it("cleans Uint8Array input", () => {
|
|
106
127
|
const str = "admin@gmail.com";
|
|
107
128
|
const buffer = new TextEncoder().encode(str);
|
|
108
|
-
const result =
|
|
129
|
+
const result = _recursiveObjectClean(buffer, "log", [emailRedactor]);
|
|
109
130
|
const decoded = new TextDecoder().decode(result as Uint8Array);
|
|
110
131
|
|
|
111
132
|
expect(decoded).toBe("[REDACTED EMAIL]");
|
|
@@ -113,7 +134,7 @@ describe("PII Detection Utils", () => {
|
|
|
113
134
|
|
|
114
135
|
it("skips malformed Uint8Array decode", () => {
|
|
115
136
|
const corrupted = new Uint8Array([0xff, 0xfe, 0xfd]);
|
|
116
|
-
const result =
|
|
137
|
+
const result = _recursiveObjectClean(corrupted, "log", [emailRedactor]);
|
|
117
138
|
|
|
118
139
|
// Should return a Uint8Array, but unmodified/redaction should not happen
|
|
119
140
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
@@ -121,8 +142,9 @@ describe("PII Detection Utils", () => {
|
|
|
121
142
|
});
|
|
122
143
|
|
|
123
144
|
it("cleans arrays of values", () => {
|
|
124
|
-
const result =
|
|
145
|
+
const result = _recursiveObjectClean(
|
|
125
146
|
["bob@abc.com", 123, { nested: "jane@example.com" }],
|
|
147
|
+
"log",
|
|
126
148
|
[emailRedactor],
|
|
127
149
|
);
|
|
128
150
|
|
|
@@ -134,10 +156,110 @@ describe("PII Detection Utils", () => {
|
|
|
134
156
|
});
|
|
135
157
|
|
|
136
158
|
it("passes null and boolean through", () => {
|
|
137
|
-
expect(
|
|
138
|
-
expect(
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
expect(_recursiveObjectClean(null, "log", [emailRedactor])).toBeNull();
|
|
160
|
+
expect(
|
|
161
|
+
_recursiveObjectClean(undefined, "log", [emailRedactor]),
|
|
162
|
+
).toBeUndefined();
|
|
163
|
+
expect(_recursiveObjectClean(true, "log", [emailRedactor])).toBe(true);
|
|
164
|
+
expect(_recursiveObjectClean(false, "log", [emailRedactor])).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("_containsEncodedComponents", () => {
|
|
169
|
+
describe("should return true for properly URL encoded strings", () => {
|
|
170
|
+
it.each([
|
|
171
|
+
["hello%20world", "Space encoded as %20"],
|
|
172
|
+
["test%2Bvalue", "Plus sign encoded as %2B"],
|
|
173
|
+
["path%2Fto%2Ffile", "Forward slashes encoded"],
|
|
174
|
+
["user%40domain.com", "@ symbol encoded"],
|
|
175
|
+
["100%25%20off", "Percent and space encoded"],
|
|
176
|
+
["a%3Db%26c%3Dd", "Query parameters (a=b&c=d)"],
|
|
177
|
+
["caf%C3%A9", "UTF-8 encoded (café)"],
|
|
178
|
+
["price%3A%20%2410", "Colon, space, dollar ($10)"],
|
|
179
|
+
["%22quoted%22", "Double quotes encoded"],
|
|
180
|
+
["https%3A%2F%2Fexample.com", "Full URL encoded"],
|
|
181
|
+
["file%20name.txt", "filename encoded"],
|
|
182
|
+
["search%3Fq%3Dhello%20world", "Query string encoded"],
|
|
183
|
+
["%3C%3E%26%22%27", "HTML special chars encoded (<>&\"')"],
|
|
184
|
+
["%E2%9C%93", "UTF-8 checkmark (✓) encoded"],
|
|
185
|
+
["test%2b", "Lowercase hex"],
|
|
186
|
+
["%E4%B8%AD%E6%96%87", "Chinese characters encoded"],
|
|
187
|
+
])('should detect "%s" as URL encoded (%s)', (input, description) => {
|
|
188
|
+
expect(_containsEncodedComponents(input)).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("should return false for non-URL encoded strings", () => {
|
|
193
|
+
it.each([
|
|
194
|
+
["test", "Simple ASCII string"],
|
|
195
|
+
["hello world", "Unencoded space"],
|
|
196
|
+
["user@domain.com", "Unencoded email"],
|
|
197
|
+
["simple123", "Alphanumeric only"],
|
|
198
|
+
["", "Empty string"],
|
|
199
|
+
["25%%", "Literal percent signs"],
|
|
200
|
+
["100% off", "Percent without hex digits"],
|
|
201
|
+
["test%2", "Incomplete percent encoding"],
|
|
202
|
+
["hello%ZZ", "Invalid hex digits"],
|
|
203
|
+
["test%2G", "Invalid hex digit G"],
|
|
204
|
+
["bad%encoding%here", "Percent without hex pairs"],
|
|
205
|
+
["hello%20world and more", "Partially encoded string"],
|
|
206
|
+
["hello%20world%21%20how%20are%20you", "Overly encoded string"],
|
|
207
|
+
["café", "Unicode characters (unencoded)"],
|
|
208
|
+
["hello+world", "Plus sign (form encoding style)"],
|
|
209
|
+
["test%", "Trailing percent"],
|
|
210
|
+
["hello%20world%", "Encoded content with trailing percent"],
|
|
211
|
+
["%", "Single percent"],
|
|
212
|
+
["%%", "Double percent"],
|
|
213
|
+
["normal text with % symbols", "Text with percent but no encoding"],
|
|
214
|
+
["price: $100%", "Currency with percent"],
|
|
215
|
+
["file.txt", "Simple filename"],
|
|
216
|
+
["path/to/file", "Unencoded path"],
|
|
217
|
+
["query?param=value", "Unencoded query string"],
|
|
218
|
+
["hello%world", "Percent without following hex"],
|
|
219
|
+
["test%1", "Single hex digit after percent"],
|
|
220
|
+
["hello%zz", "Non-hex characters after percent"],
|
|
221
|
+
])('should not detect "%s" as URL encoded (%s)', (input, description) => {
|
|
222
|
+
expect(_containsEncodedComponents(input)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("Error handling", () => {
|
|
227
|
+
it.each([
|
|
228
|
+
["%C0%80", "Overlong UTF-8 encoding (security concern)"],
|
|
229
|
+
["%ED%A0%80", "UTF-8 surrogate (invalid)"],
|
|
230
|
+
["%FF%FE", "Invalid UTF-8 sequence"],
|
|
231
|
+
["test%20%ZZ", "Mix of valid and invalid encoding"],
|
|
232
|
+
])('should handle "%s" (%s)', (input, description) => {
|
|
233
|
+
// These should not throw errors
|
|
234
|
+
expect(() => _containsEncodedComponents(input)).not.toThrow();
|
|
235
|
+
|
|
236
|
+
// Most of these should return false due to invalid sequences
|
|
237
|
+
// or security-related encoding issues
|
|
238
|
+
const result = _containsEncodedComponents(input);
|
|
239
|
+
expect(typeof result).toBe("boolean");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("real-world url examples", () => {
|
|
244
|
+
it.each([
|
|
245
|
+
[
|
|
246
|
+
"https%3A%2F%2Fgoogle.com%2Fsearch%3Fq%3Djavascript",
|
|
247
|
+
"Encoded Google search URL",
|
|
248
|
+
],
|
|
249
|
+
["The%20quick%20brown%20fox", "Sentence with spaces encoded"],
|
|
250
|
+
[
|
|
251
|
+
"/api/user?body=here%20are%20all%20my%20secrets",
|
|
252
|
+
"contextually encoded API path",
|
|
253
|
+
],
|
|
254
|
+
["redirect_uri=https%3A%2F%2Fapp.com%2Fcallback", "OAuth redirect URI"],
|
|
255
|
+
["data%3Atext%2Fplain%3Bbase64%2CSGVsbG8%3D", "Data URL encoded"],
|
|
256
|
+
])('real-world case: "%s" (%s)', (input, description) => {
|
|
257
|
+
const result = _containsEncodedComponents(input);
|
|
258
|
+
|
|
259
|
+
// Verify the function doesn't crash
|
|
260
|
+
expect(typeof result).toBe("boolean");
|
|
261
|
+
expect(result).toBe(true);
|
|
262
|
+
});
|
|
141
263
|
});
|
|
142
264
|
});
|
|
143
265
|
});
|
|
@@ -67,9 +67,13 @@ describe("IP Redaction utils", () => {
|
|
|
67
67
|
${"256.1.1.1"} | ${"256.1.1.1"}
|
|
68
68
|
${"0.0.0.0!"} | ${"[REDACTED IPV4]!"}
|
|
69
69
|
${"text0.0.0.0"} | ${"text[REDACTED IPV4]"}
|
|
70
|
+
${"%A00.0.0.0"} | ${"[REDACTED IPV4]"}
|
|
71
|
+
${"0.0.0.0%20"} | ${"[REDACTED IPV4]"}
|
|
70
72
|
${"0.0.text0.0"} | ${"0.0.text0.0"}
|
|
71
73
|
${"2001:0db8::1"} | ${"[REDACTED IPV6]"}
|
|
72
74
|
${"::1"} | ${"[REDACTED IPV6]"}
|
|
75
|
+
${"%20::1"} | ${"[REDACTED IPV6]"}
|
|
76
|
+
${"::1%A0"} | ${"[REDACTED IPV6]"}
|
|
73
77
|
${"text::1"} | ${"text[REDACTED IPV6]"}
|
|
74
78
|
${"::1text"} | ${"[REDACTED IPV6]text"}
|
|
75
79
|
${"sentence ending with f::1"} | ${"sentence ending with [REDACTED IPV6]"}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as piiDetection from "../../lib/internals/redaction/pii-detection.js";
|
|
3
|
-
import { getActiveSpan } from "../../lib/traces.js";
|
|
4
|
-
import { MockSpan } from "../utils/mock-signals.js";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
2
|
import { setNodeSdkConfig } from "../../lib/config-manager.js";
|
|
3
|
+
import { getActiveSpan } from "../../lib/traces.js";
|
|
6
4
|
|
|
7
5
|
describe("getActiveSpan", () => {
|
|
8
6
|
it("returns undefined if no active span", async () => {
|