Files
4f458a0fab test(sdr): add traces SDR e2e suite (redact/drop/hash × ingestion/query) (#13390)
## What

Adds an end-to-end **traces SDR (Sensitive Data Redaction)** test suite,
extending SDR coverage from logs to traces. This validates the fix in
this PR branch (`feat/fix-traces-sdr-bug`) — SDR regex patterns were
previously applied to logs only; these tests assert the same policies
now apply to **trace span attributes** at both ingestion and query time.

Targets `feat/fix-traces-sdr-bug` so the tests land together with the
fix they cover.

## Tests added

**6 new spec files → 12 tests** (each spec = 1 `setup: cleanup patterns`
+ 1 combined functional test), covering **24 field-level policy
assertions** (4 sensitive fields × 6 specs). All tagged `@enterprise
@sdr @traces`.

| # | Spec | Policy | Timing | What it asserts |
|---|------|--------|--------|-----------------|
| 1 | `tracesIngestionTimeRedaction.spec.js` | Redact | Ingestion |
Span-attribute values matching the pattern are stored as `[REDACTED]` |
| 2 | `tracesQueryTimeRedaction.spec.js` | Redact | Query | Stored
values are untouched; the search response returns `[REDACTED]` |
| 3 | `tracesIngestionTimeDrop.spec.js` | Drop | Ingestion | Matching
fields are removed from the record at write time |
| 4 | `tracesQueryTimeDrop.spec.js` | Drop | Query | Fields present in
storage are dropped from the search response |
| 5 | `tracesIngestionTimeHash.spec.js` | Hash | Ingestion | Matching
values are stored as `[REDACTED:<hash>]` |
| 6 | `tracesQueryTimeHash.spec.js` | Hash | Query | Search response
returns `[REDACTED:<hash>]`; storage untouched |

Each combined test drives the full flow for **4 fields / patterns**:

- **email** (`user_email` = `john.doe@example.com`) —
`^[\w\.-]+@[\w\.-]+\.\w{2,}$`
- **US phone** (`phone` = `5551234567`) — `^\d{10}$`
- **credit card** (`cc_number` = `1234 5678 9012 3456`) —
`^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$`
- **SSN** (`ssn` = `123-45-6789`) — `^\d{3}-\d{2}-\d{4}$`

(drop/hash specs use `website_url`, `user_password`, `server_ip`,
`theme_color` with URL / password / IPv4 / hex-color patterns.)

Each functional test follows: ingest → assert field visible (no pattern)
→ create 4 patterns → associate to the traces stream's fields →
ingest/re-query → assert the field is redacted / dropped / hashed.

## How it works

Traces reuse the **shared** pattern create/associate/verify flow with
logs — only the ingest and read seams differ:

- **`sdrTracesIngestionPage.js`** (new): ingests via OTLP
`/api/{org}/v1/traces` (`stream-name` header). Each test field is a span
attribute that flattens to an identical top-level field; every span also
carries an `sdr_test_id` marker for batch isolation.
- **`sdrVerificationPage` / `streamAssociationPage`**: parameterized by
`streamType` — `?type=traces` search, Traces stream-type tab selection,
and schema-field filtering so a field row in the (virtualized,
~20-field) traces schema is reliably found.
- **`apiCleanup`**: `fetchStreams` / `deleteStream` / `cleanupStreams` /
`isStreamStillDeleting` / `waitForStreamDeletion` take an optional
`streamType` (default `'logs'` — no change for existing callers).

## Artifact cleanup

- `cleanup.spec.js` sweeps `sdr_traces_*` streams via `streamType:
'traces'` (the logs sweep is `type=logs`-only and never listed them).
- `cleanupRegexPatterns` adds the `traces_` prefix.
- **Verified live**: the sweep deleted 8 leftover traces streams + 32
traces patterns; 0 remained.

## Verification

- All **6 specs / 12 tests pass** against a local enterprise build
carrying the fix.
- The existing **logs** SDR suite still passes unchanged (no regression
from the shared page-object changes).

## Companion

CI wiring is a paired o2-enterprise PR (branch `test/traces-sdr-e2e`)
that splits the SDR matrix into `SDR-Logs` and `SDR-Traces` shards so
the new specs run as their own shard.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Chaitanya Sistla <sistlachaitanya2@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 06:46:10 +00:00

170 lines
7.1 KiB
JavaScript

const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const PageManager = require('../../pages/page-manager.js');
// Traces variant of the SDR query-time hash suite. Data stays intact in storage;
// matching values are replaced with [REDACTED:<hash>] in the search response at
// query time. One ingested batch is inspected both before (visible) and after
// (hashed) the pattern is applied.
test.describe("Traces Query Time Hash - Combined Test", { tag: '@enterprise' }, () => {
test.describe.configure({ mode: 'serial' });
let pm;
// Generate unique test run ID for isolation
const testRunId = Date.now().toString(36);
// All patterns used in this spec file - with full pattern definitions for uniqueness
const patternsToTest = [
{
name: `traces_url_query_hash_${testRunId}`,
description: 'URL validation pattern (for traces query hash tests)',
pattern: '^https?://[\\w.-]+\\.[\\w.-]+.*$',
field: 'website_url',
value: 'https://example.com'
},
{
name: `traces_password_query_hash_${testRunId}`,
description: 'Strong password pattern (for traces query hash tests)',
pattern: '^[A-Za-z0-9@$!%*?&]{8,}$',
field: 'user_password',
value: 'P@ssw0rd!123'
},
{
name: `traces_ipv4_query_hash_${testRunId}`,
description: 'IPv4 address format (for traces query hash tests)',
pattern: '^(\\d{1,3}\\.){3}\\d{1,3}$',
field: 'server_ip',
value: '192.168.1.1'
},
{
name: `traces_hex_color_query_hash_${testRunId}`,
description: 'Hexadecimal color code (for traces query hash tests)',
pattern: '^#[0-9a-fA-F]{6}$',
field: 'theme_color',
value: '#FF5733'
}
];
// Use unique stream name to avoid "stream being deleted" conflicts
const testStreamName = `sdr_traces_query_hash_${testRunId}`;
const streamType = 'traces';
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
testLogger.info('Combined traces query time hash test setup completed');
});
test("setup: cleanup patterns", {
tag: ['@sdr', '@traces', '@cleanup', '@queryHash']
}, async ({ page }) => {
testLogger.info('=== SPEC-LEVEL CLEANUP: Cleaning up patterns for traces query hash test ===');
for (const pattern of patternsToTest) {
testLogger.info(`Checking and cleaning up pattern: ${pattern.name}`);
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.checkPatternExists(pattern.name);
if (exists) {
testLogger.info(`Pattern ${pattern.name} exists. Deleting (and unlinking if needed).`);
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(pattern.name);
if (!deleteResult.success && deleteResult.reason === 'in_use') {
testLogger.info('Pattern in use, unlinking from streams first');
for (const association of deleteResult.associations) {
await pm.streamAssociationPage.unlinkPatternFromField(
association.streamName,
association.fieldName,
pattern.name,
streamType
);
}
await pm.sdrPatternsPage.navigateToRegexPatterns();
await pm.sdrPatternsPage.deletePatternByName(pattern.name);
}
testLogger.info(`✓ Pattern ${pattern.name} deleted successfully`);
} else {
testLogger.info(`✓ Pattern ${pattern.name} does not exist, no cleanup needed`);
}
}
testLogger.info('=== SPEC-LEVEL CLEANUP COMPLETE ===');
});
test("should hash multiple trace fields at query time when patterns are applied", {
tag: ['@sdr', '@traces', '@queryHash', '@all', '@combined']
}, async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'dependency', description: 'setup: cleanup patterns' });
testLogger.info('=== Testing combined traces hash for 4 patterns at query time ===');
// STEP 1: Ingest trace data once (data will remain unchanged in storage)
testLogger.info('STEP 1: Ingest 4 trace spans (before any patterns are applied)');
const dataToIngest = patternsToTest.map(p => ({
fieldName: p.field,
fieldValue: p.value
}));
// Query-time SDR transforms data at search time, so STEP 1 (visible) and STEP 4
// (hashed) inspect the SAME ingested batch — reuse one marker for both.
const ingestMarker = await pm.sdrTracesPage.ingestMultipleFields(testStreamName, dataToIngest);
// Verify all fields are visible without hashing
const fieldsBeforeHash = patternsToTest.map(p => ({
fieldName: p.field,
shouldBeHashed: false
}));
await pm.sdrVerificationPage.verifyMultipleFields(pm.logsPage, testStreamName, fieldsBeforeHash, ingestMarker, streamType);
testLogger.info('✓ STEP 1 PASSED: All trace fields visible without SDR');
// STEP 2: Create all 4 SDR patterns
testLogger.info('STEP 2: Create all 4 SDR patterns');
for (const patternConfig of patternsToTest) {
await pm.sdrPatternsPage.navigateToRegexPatterns();
await pm.sdrPatternsPage.createPattern(patternConfig.name, patternConfig.description, patternConfig.pattern);
await pm.sdrPatternsPage.verifyPatternCreatedSuccess();
await pm.sdrPatternsPage.navigateToRegexPatterns();
const exists = await pm.sdrPatternsPage.waitForPatternCreated(patternConfig.name); // backend-consistent create gate (API source of truth)
expect(exists).toBeTruthy();
testLogger.info(`✓ Created and verified pattern: ${patternConfig.name}`);
}
testLogger.info('✓ STEP 2 PASSED: All 4 patterns created and verified');
// STEP 3: Link all 4 patterns to their respective trace fields with QUERY TIME hash
testLogger.info('STEP 3: Link all 4 patterns to their respective trace fields with HASH action');
for (const patternConfig of patternsToTest) {
await pm.streamAssociationPage.associatePatternWithStream(
testStreamName,
patternConfig.name,
'hash',
'query', // QUERY TIME - key difference from ingestion time
patternConfig.field,
streamType
);
testLogger.info(`✓ Linked pattern ${patternConfig.name} to field ${patternConfig.field} with HASH action`);
}
testLogger.info('✓ STEP 3 PASSED: All 4 patterns linked to trace fields with HASH action');
// STEP 4: Verify all fields are HASHED at query time (NO RE-INGESTION)
testLogger.info('STEP 4: Verify trace fields are HASHED at query time (data unchanged in storage)');
const fieldsAfterHash = patternsToTest.map(p => ({
fieldName: p.field,
shouldBeHashed: true
}));
await pm.sdrVerificationPage.verifyMultipleFields(pm.logsPage, testStreamName, fieldsAfterHash, ingestMarker, streamType);
testLogger.info('✓ STEP 4 PASSED: All trace fields are HASHED at query time');
testLogger.info('=== ✓ COMBINED TRACES QUERY TIME HASH TEST COMPLETED SUCCESSFULLY ===');
});
});