@reproapp/node-sdk 0.0.57 → 0.0.58
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/README.md +11 -4
- package/dist/endpoints.d.ts +2 -0
- package/dist/endpoints.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -5
- package/dist/ingest/client.js +2 -2
- package/dist/ingest/types.d.ts +1 -0
- package/dist/privacy.d.ts +1 -1
- package/dist/privacy.js +5 -2
- package/package.json +2 -1
- package/src/endpoints.ts +2 -0
- package/src/index.ts +5 -5
- package/src/ingest/client.ts +1 -1
- package/src/ingest/types.ts +1 -0
- package/src/privacy.ts +5 -2
- package/test/hosted-endpoints.testcontainers.test.js +99 -0
package/README.md
CHANGED
|
@@ -22,6 +22,14 @@ Requirements:
|
|
|
22
22
|
|
|
23
23
|
## Quickstart
|
|
24
24
|
|
|
25
|
+
API and collector URLs are optional. Without overrides, the SDK uses
|
|
26
|
+
`https://repro-api-d7288.ondigitalocean.app/api` and
|
|
27
|
+
`https://log-collector-5rj86.ondigitalocean.app`.
|
|
28
|
+
For local or custom deployments, set `apiBase` and `ingestBase`, or use
|
|
29
|
+
`REPRO_API_BASE_URL`/`REPRO_API_BASE` and `REPRO_INGEST_BASE`.
|
|
30
|
+
Policy fetching also uses `apiBase` unless `privacy.apiBase` or
|
|
31
|
+
`REPRO_PRIVACY_API_BASE` overrides it. Existing environment overrides still apply.
|
|
32
|
+
|
|
25
33
|
Call `initRepro` before importing your application modules. This lets the SDK
|
|
26
34
|
install tracing hooks, auto-attach HTTP middleware to Express/Nest apps, register
|
|
27
35
|
database/message/email integrations, and fetch the runtime privacy policy.
|
|
@@ -36,14 +44,12 @@ async function bootstrap() {
|
|
|
36
44
|
appSecret: process.env.REPRO_APP_SECRET!,
|
|
37
45
|
appName: process.env.REPRO_APP_NAME ?? "Acme Web",
|
|
38
46
|
serviceName: process.env.REPRO_SERVICE_NAME ?? "api",
|
|
39
|
-
apiBase: process.env.REPRO_API_BASE,
|
|
40
47
|
tracing: {
|
|
41
48
|
disableFunctionTypes: ["constructor"],
|
|
42
49
|
logFunctionCalls: false,
|
|
43
50
|
},
|
|
44
51
|
privacy: {
|
|
45
52
|
environment: process.env.REPRO_PRIVACY_ENV ?? process.env.NODE_ENV ?? "dev",
|
|
46
|
-
apiBase: process.env.REPRO_PRIVACY_API_BASE ?? process.env.REPRO_API_BASE,
|
|
47
53
|
},
|
|
48
54
|
});
|
|
49
55
|
|
|
@@ -107,7 +113,7 @@ await initRepro({
|
|
|
107
113
|
appName: "Acme Web",
|
|
108
114
|
serviceName: "api",
|
|
109
115
|
apiBase: process.env.REPRO_API_BASE,
|
|
110
|
-
ingestBase: process.env.REPRO_INGEST_BASE
|
|
116
|
+
ingestBase: process.env.REPRO_INGEST_BASE,
|
|
111
117
|
});
|
|
112
118
|
```
|
|
113
119
|
|
|
@@ -242,7 +248,8 @@ Important options:
|
|
|
242
248
|
- `serviceName`: Backend service label stored on request, trace, DB, queue, Redis,
|
|
243
249
|
and email events. The SDK also registers this service with the application in
|
|
244
250
|
the background so admins can see its environments and privacy policy status.
|
|
245
|
-
- `apiBase`: Repro API base URL.
|
|
251
|
+
- `apiBase`: Optional Repro API base URL. Defaults to the hosted API and is used by backend live sessions and privacy policy fetches.
|
|
252
|
+
- `ingestBase`: Optional collector base URL. Defaults to the hosted collector; `REPRO_INGEST_BASE` takes precedence when set.
|
|
246
253
|
- `captureHeaders`: Enables header capture. Sensitive headers are masked unless
|
|
247
254
|
explicitly unmasked.
|
|
248
255
|
- `masking`: Local masking rules for request bodies, response bodies, and trace values.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_INGEST_BASE = exports.DEFAULT_API_BASE = void 0;
|
|
4
|
+
exports.DEFAULT_API_BASE = 'https://repro-api-d7288.ondigitalocean.app/api';
|
|
5
|
+
exports.DEFAULT_INGEST_BASE = 'https://log-collector-5rj86.ondigitalocean.app';
|
package/dist/index.d.ts
CHANGED
|
@@ -324,7 +324,7 @@ declare function chunkTraceEventsForTransport(events: TraceEventRecord[], reques
|
|
|
324
324
|
export type ReproMiddlewareConfig = IngestClientConfig & {
|
|
325
325
|
/** Propagated recording mode. A tagged staging request never inherits live limits. */
|
|
326
326
|
captureMode?: 'live' | 'staging';
|
|
327
|
-
/** API base
|
|
327
|
+
/** API base for live sessions and privacy. Uses environment/privacy overrides, then the hosted Repro API. */
|
|
328
328
|
apiBase?: string;
|
|
329
329
|
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
330
330
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const crypto_1 = require("crypto");
|
|
|
25
25
|
const module_1 = require("module");
|
|
26
26
|
const path_1 = require("path");
|
|
27
27
|
const url_1 = require("url");
|
|
28
|
+
const endpoints_1 = require("./endpoints");
|
|
28
29
|
const node_v8_1 = require("node:v8");
|
|
29
30
|
const client_1 = require("./ingest/client");
|
|
30
31
|
const privacy_1 = require("./privacy");
|
|
@@ -325,7 +326,6 @@ const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
|
|
|
325
326
|
const KAFKA_HEADER_REQUEST_ID = 'x-repro-request-rid';
|
|
326
327
|
const KAFKA_HEADER_SESSION_ID = 'x-bug-session-id';
|
|
327
328
|
const KAFKA_HEADER_ACTION_ID = 'x-bug-action-id';
|
|
328
|
-
const DEFAULT_INGEST_BASE = 'http://localhost:8080';
|
|
329
329
|
const DEFAULT_INGEST_PATH = '/v1/ingest/events';
|
|
330
330
|
const DEFAULT_LIVE_SESSION_DURATION_MS = 3 * 60000;
|
|
331
331
|
const DEFAULT_LIVE_SEGMENT_IDLE_MS = 15000;
|
|
@@ -1846,7 +1846,7 @@ function resolveLiveApiBase(cfg, liveMode) {
|
|
|
1846
1846
|
env?.REPRO_API_BASE_URL ||
|
|
1847
1847
|
env?.REPRO_API_BASE ||
|
|
1848
1848
|
cfg.privacy?.apiBase ||
|
|
1849
|
-
|
|
1849
|
+
endpoints_1.DEFAULT_API_BASE;
|
|
1850
1850
|
if (typeof raw !== 'string' || !raw.trim())
|
|
1851
1851
|
return null;
|
|
1852
1852
|
return raw.trim().replace(/\/+$/, '');
|
|
@@ -2123,7 +2123,7 @@ async function resolveBackendLiveContext(cfg, req, requestStart) {
|
|
|
2123
2123
|
}
|
|
2124
2124
|
function resolveIngestUrl(cfg) {
|
|
2125
2125
|
const envBase = typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_BASE : undefined;
|
|
2126
|
-
const ingestBase = String(envBase || cfg.ingestBase || DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
2126
|
+
const ingestBase = String(envBase || cfg.ingestBase || endpoints_1.DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
2127
2127
|
const ingestPath = cfg.ingestPath || DEFAULT_INGEST_PATH;
|
|
2128
2128
|
const normalizedPath = ingestPath.startsWith('/') ? ingestPath : `/${ingestPath}`;
|
|
2129
2129
|
return `${ingestBase}${normalizedPath}`;
|
|
@@ -3422,7 +3422,7 @@ async function ensureRuntimePrivacyPolicyLoaded(cfg) {
|
|
|
3422
3422
|
if (state.nextRetryAt && Date.now() < state.nextRetryAt) {
|
|
3423
3423
|
return state.policy;
|
|
3424
3424
|
}
|
|
3425
|
-
const apiBase = (0, privacy_1.resolvePrivacyApiBase)(privacy);
|
|
3425
|
+
const apiBase = (0, privacy_1.resolvePrivacyApiBase)(privacy, cfg.apiBase);
|
|
3426
3426
|
if (!apiBase || !cfg.appId || !cfg.appSecret) {
|
|
3427
3427
|
reportRuntimePrivacyOutcome(cfg, state, `disabled:${!apiBase ? 'missing_config' : 'missing_credentials'}:${apiBase ?? 'no-api-base'}`, !apiBase
|
|
3428
3428
|
? 'runtime privacy is enabled but no privacy API base is configured'
|
|
@@ -3459,7 +3459,7 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg) {
|
|
|
3459
3459
|
if (state.policy) {
|
|
3460
3460
|
return;
|
|
3461
3461
|
}
|
|
3462
|
-
const apiBase = (0, privacy_1.resolvePrivacyApiBase)(privacy);
|
|
3462
|
+
const apiBase = (0, privacy_1.resolvePrivacyApiBase)(privacy, cfg.apiBase);
|
|
3463
3463
|
if (!apiBase) {
|
|
3464
3464
|
return;
|
|
3465
3465
|
}
|
package/dist/ingest/client.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.flushIngestQueue = exports.enqueueIngestEntries = exports.postIngestEntries = exports.kickIngestQueueDrain = exports.configureIngestQueueDrain = void 0;
|
|
4
4
|
const mapper_1 = require("./mapper");
|
|
5
|
-
const
|
|
5
|
+
const endpoints_1 = require("../endpoints");
|
|
6
6
|
const DEFAULT_INGEST_PATH = '/v1/ingest/events';
|
|
7
7
|
const DEFAULT_QUEUE_MAX_ITEMS = (() => {
|
|
8
8
|
const env = Number(typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_QUEUE_MAX_ITEMS : undefined);
|
|
@@ -145,7 +145,7 @@ const selectSendBatch = (queue) => {
|
|
|
145
145
|
};
|
|
146
146
|
const resolveIngestUrl = (config) => {
|
|
147
147
|
const envBase = typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_BASE : undefined;
|
|
148
|
-
const ingestBase = String(envBase || config.ingestBase || DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
148
|
+
const ingestBase = String(envBase || config.ingestBase || endpoints_1.DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
149
149
|
const ingestPath = config.ingestPath || DEFAULT_INGEST_PATH;
|
|
150
150
|
const normalizedPath = ingestPath.startsWith('/') ? ingestPath : `/${ingestPath}`;
|
|
151
151
|
return `${ingestBase}${normalizedPath}`;
|
package/dist/ingest/types.d.ts
CHANGED
package/dist/privacy.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export type RuntimePrivacyFetchResult = {
|
|
|
94
94
|
error: string;
|
|
95
95
|
};
|
|
96
96
|
export declare function normalizePrivacyEnvironment(input?: string | null): string;
|
|
97
|
-
export declare function resolvePrivacyApiBase(cfg?: RuntimePrivacyConfig | null): string | null;
|
|
97
|
+
export declare function resolvePrivacyApiBase(cfg?: RuntimePrivacyConfig | null, apiBase?: string): string | null;
|
|
98
98
|
export declare function fetchApprovedRuntimePrivacyPolicy(params: {
|
|
99
99
|
appId: string;
|
|
100
100
|
appSecret?: string;
|
package/dist/privacy.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.normalizeRuntimePrivacyPolicy = normalizeRuntimePrivacyPolicy;
|
|
|
8
8
|
exports.applyRuntimePrivacyPolicy = applyRuntimePrivacyPolicy;
|
|
9
9
|
exports.applyRuntimePrivacyPolicyAsync = applyRuntimePrivacyPolicyAsync;
|
|
10
10
|
const crypto_1 = require("crypto");
|
|
11
|
+
const endpoints_1 = require("./endpoints");
|
|
11
12
|
const capture_work_budget_1 = require("./capture-work-budget");
|
|
12
13
|
const privacy_fallback_1 = require("./privacy-fallback");
|
|
13
14
|
const privacy_redaction_1 = require("./privacy-redaction");
|
|
@@ -84,11 +85,13 @@ function normalizePrivacyEnvironment(input) {
|
|
|
84
85
|
return 'qa';
|
|
85
86
|
return normalized || 'dev';
|
|
86
87
|
}
|
|
87
|
-
function resolvePrivacyApiBase(cfg) {
|
|
88
|
+
function resolvePrivacyApiBase(cfg, apiBase) {
|
|
88
89
|
const value = cfg?.apiBase ||
|
|
89
90
|
process.env.REPRO_PRIVACY_API_BASE ||
|
|
91
|
+
apiBase ||
|
|
92
|
+
process.env.REPRO_API_BASE_URL ||
|
|
90
93
|
process.env.REPRO_API_BASE ||
|
|
91
|
-
|
|
94
|
+
endpoints_1.DEFAULT_API_BASE;
|
|
92
95
|
if (!value || !String(value).trim()) {
|
|
93
96
|
return null;
|
|
94
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"node": ">=18"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
+
"test:endpoints": "npm run build && node test/hosted-endpoints.testcontainers.test.js",
|
|
12
13
|
"test:throughput": "npm run build && node test/trace-worker-policy-reference.test.js && node test/privacy-policy-index.test.js && node test/trace-throughput.testcontainers.test.js",
|
|
13
14
|
"test:latency": "npm run build && node test/privacy-fetch-timeout.test.js && node test/capture-latency.testcontainers.test.js",
|
|
14
15
|
"test:live-capture": "npm run build && node test/twelve-element-capture.testcontainers.test.js && node test/live-capture-limits.test.js && node test/ingest-throttle-retention.test.js && node test/live-capture-limits.testcontainers.test.js",
|
package/src/endpoints.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash, createHmac, randomUUID } from 'crypto';
|
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { pathToFileURL } from 'url';
|
|
9
|
+
import { DEFAULT_API_BASE, DEFAULT_INGEST_BASE } from './endpoints';
|
|
9
10
|
import { serialize as serializeSnapshot } from 'node:v8';
|
|
10
11
|
import {
|
|
11
12
|
configureIngestQueueDrain,
|
|
@@ -325,7 +326,6 @@ const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
|
|
|
325
326
|
const KAFKA_HEADER_REQUEST_ID = 'x-repro-request-rid';
|
|
326
327
|
const KAFKA_HEADER_SESSION_ID = 'x-bug-session-id';
|
|
327
328
|
const KAFKA_HEADER_ACTION_ID = 'x-bug-action-id';
|
|
328
|
-
const DEFAULT_INGEST_BASE = 'http://localhost:8080';
|
|
329
329
|
const DEFAULT_INGEST_PATH = '/v1/ingest/events';
|
|
330
330
|
const DEFAULT_LIVE_SESSION_DURATION_MS = 3 * 60_000;
|
|
331
331
|
const DEFAULT_LIVE_SEGMENT_IDLE_MS = 15_000;
|
|
@@ -2317,7 +2317,7 @@ function resolveLiveApiBase(cfg: ReproMiddlewareConfig, liveMode?: ReproLiveMode
|
|
|
2317
2317
|
env?.REPRO_API_BASE_URL ||
|
|
2318
2318
|
env?.REPRO_API_BASE ||
|
|
2319
2319
|
(cfg.privacy as RuntimePrivacyConfig | undefined)?.apiBase ||
|
|
2320
|
-
|
|
2320
|
+
DEFAULT_API_BASE;
|
|
2321
2321
|
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
2322
2322
|
return raw.trim().replace(/\/+$/, '');
|
|
2323
2323
|
}
|
|
@@ -4192,7 +4192,7 @@ async function ensureRuntimePrivacyPolicyLoaded(cfg: ReproMiddlewareConfig): Pro
|
|
|
4192
4192
|
if (state.nextRetryAt && Date.now() < state.nextRetryAt) {
|
|
4193
4193
|
return state.policy;
|
|
4194
4194
|
}
|
|
4195
|
-
const apiBase = resolvePrivacyApiBase(privacy);
|
|
4195
|
+
const apiBase = resolvePrivacyApiBase(privacy, cfg.apiBase);
|
|
4196
4196
|
if (!apiBase || !cfg.appId || !cfg.appSecret) {
|
|
4197
4197
|
reportRuntimePrivacyOutcome(
|
|
4198
4198
|
cfg,
|
|
@@ -4241,7 +4241,7 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg: ReproMiddlewareConfi
|
|
|
4241
4241
|
return;
|
|
4242
4242
|
}
|
|
4243
4243
|
|
|
4244
|
-
const apiBase = resolvePrivacyApiBase(privacy);
|
|
4244
|
+
const apiBase = resolvePrivacyApiBase(privacy, cfg.apiBase);
|
|
4245
4245
|
if (!apiBase) {
|
|
4246
4246
|
return;
|
|
4247
4247
|
}
|
|
@@ -5996,7 +5996,7 @@ async function maybeCaptureDbValueFromCandidatesAsync(
|
|
|
5996
5996
|
export type ReproMiddlewareConfig = IngestClientConfig & {
|
|
5997
5997
|
/** Propagated recording mode. A tagged staging request never inherits live limits. */
|
|
5998
5998
|
captureMode?: 'live' | 'staging';
|
|
5999
|
-
/** API base
|
|
5999
|
+
/** API base for live sessions and privacy. Uses environment/privacy overrides, then the hosted Repro API. */
|
|
6000
6000
|
apiBase?: string;
|
|
6001
6001
|
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
6002
6002
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
package/src/ingest/client.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mapLegacyEntriesToCanonicalEvents } from './mapper';
|
|
2
2
|
import type { IngestClientConfig, LegacyEntry } from './types';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import { DEFAULT_INGEST_BASE } from '../endpoints';
|
|
5
5
|
const DEFAULT_INGEST_PATH = '/v1/ingest/events';
|
|
6
6
|
const DEFAULT_QUEUE_MAX_ITEMS = (() => {
|
|
7
7
|
const env = Number(typeof process !== 'undefined' ? (process as any)?.env?.REPRO_INGEST_QUEUE_MAX_ITEMS : undefined);
|
package/src/ingest/types.ts
CHANGED
package/src/privacy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { DEFAULT_API_BASE } from './endpoints';
|
|
2
3
|
import { yieldCaptureWork } from './capture-work-budget';
|
|
3
4
|
import { sanitizeUnknownBoundaryTextWithDetectors } from './privacy-fallback';
|
|
4
5
|
import { redactEmailDeterministic, redactEmailsInText } from './privacy-redaction';
|
|
@@ -227,12 +228,14 @@ export function normalizePrivacyEnvironment(input?: string | null): string {
|
|
|
227
228
|
return normalized || 'dev';
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
export function resolvePrivacyApiBase(cfg?: RuntimePrivacyConfig | null): string | null {
|
|
231
|
+
export function resolvePrivacyApiBase(cfg?: RuntimePrivacyConfig | null, apiBase?: string): string | null {
|
|
231
232
|
const value =
|
|
232
233
|
cfg?.apiBase ||
|
|
233
234
|
process.env.REPRO_PRIVACY_API_BASE ||
|
|
235
|
+
apiBase ||
|
|
236
|
+
process.env.REPRO_API_BASE_URL ||
|
|
234
237
|
process.env.REPRO_API_BASE ||
|
|
235
|
-
|
|
238
|
+
DEFAULT_API_BASE;
|
|
236
239
|
if (!value || !String(value).trim()) {
|
|
237
240
|
return null;
|
|
238
241
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
const { GenericContainer, Wait } = require('testcontainers');
|
|
4
|
+
const { MongoClient } = require('mongodb');
|
|
5
|
+
const { __reproTestHooks } = require('../dist');
|
|
6
|
+
const { resolvePrivacyApiBase, fetchApprovedRuntimePrivacyPolicy } = require('../dist/privacy');
|
|
7
|
+
const { enqueueIngestEntries, flushIngestQueue } = require('../dist/ingest/client');
|
|
8
|
+
|
|
9
|
+
const API = 'https://repro-api-d7288.ondigitalocean.app/api';
|
|
10
|
+
const COLLECTOR = 'https://log-collector-5rj86.ondigitalocean.app';
|
|
11
|
+
const envKeys = ['REPRO_API_BASE_URL', 'REPRO_API_BASE', 'REPRO_PRIVACY_API_BASE', 'REPRO_INGEST_BASE'];
|
|
12
|
+
const originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]));
|
|
13
|
+
const clearEnv = () => envKeys.forEach(key => delete process.env[key]);
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
let container, mongo, server;
|
|
17
|
+
const originalFetch = global.fetch;
|
|
18
|
+
const requests = [];
|
|
19
|
+
const errors = [];
|
|
20
|
+
try {
|
|
21
|
+
clearEnv();
|
|
22
|
+
container = await new GenericContainer('mongo:7').withExposedPorts(27017)
|
|
23
|
+
.withWaitStrategy(Wait.forListeningPorts()).start();
|
|
24
|
+
mongo = await new MongoClient(`mongodb://${container.getHost()}:${container.getMappedPort(27017)}`).connect();
|
|
25
|
+
const events = mongo.db('endpoint_defaults').collection('events');
|
|
26
|
+
server = http.createServer(async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
30
|
+
const body = chunks.length ? JSON.parse(Buffer.concat(chunks)) : {};
|
|
31
|
+
const path = new URL(req.url, 'http://local').pathname;
|
|
32
|
+
let response;
|
|
33
|
+
if (path.endsWith('/sdk/privacy-policy')) {
|
|
34
|
+
response = { enabled: true, policy: { environment: 'dev', statements: [
|
|
35
|
+
{ sid: 'protect-secret', when: { scope: 'FIELD', selector: 'field:secret' }, apply: { action: 'DROP' } },
|
|
36
|
+
] } };
|
|
37
|
+
} else if (path.endsWith('/sdk/runtime-services')) {
|
|
38
|
+
response = { ok: true };
|
|
39
|
+
} else if (path.endsWith('/ingest/events')) {
|
|
40
|
+
assert.ok(body.events.length > 0);
|
|
41
|
+
await events.insertMany(body.events);
|
|
42
|
+
response = { accepted: body.events.length };
|
|
43
|
+
} else throw new Error(`Unexpected path: ${path}`);
|
|
44
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify(response));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
errors.push(error.message);
|
|
48
|
+
res.writeHead(500); res.end('{}');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
52
|
+
const local = `http://127.0.0.1:${server.address().port}`;
|
|
53
|
+
// Assert the actual destination selected by the SDK, but never contact production.
|
|
54
|
+
const allowedOrigins = new Set([new URL(API).origin, COLLECTOR, local, 'http://override.test', 'http://environment.test']);
|
|
55
|
+
global.fetch = (url, options) => {
|
|
56
|
+
const target = new URL(url);
|
|
57
|
+
assert.ok(allowedOrigins.has(target.origin), `Unexpected external request: ${target.origin}`);
|
|
58
|
+
requests.push({ url: target.href, options });
|
|
59
|
+
return originalFetch(`${local}${target.pathname}${target.search}`, options);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const cases = [
|
|
63
|
+
{ name: 'omitted', config: {}, env: {}, api: API, privacy: API, collector: COLLECTOR },
|
|
64
|
+
{ name: 'explicit-local', config: { apiBase: `${local}/api/`, ingestBase: `${local}/` }, env: {}, api: `${local}/api`, privacy: `${local}/api`, collector: local },
|
|
65
|
+
{ name: 'environment', config: {}, env: { REPRO_API_BASE: 'http://environment.test/api/', REPRO_INGEST_BASE: 'http://environment.test/' }, api: 'http://environment.test/api', privacy: 'http://environment.test/api', collector: 'http://environment.test' },
|
|
66
|
+
{ name: 'precedence', config: { apiBase: 'http://override.test/api/', ingestBase: 'http://override.test', privacy: { apiBase: 'http://override.test/privacy/' } }, env: { REPRO_API_BASE: 'http://environment.test/api', REPRO_INGEST_BASE: 'http://environment.test', REPRO_PRIVACY_API_BASE: 'http://environment.test/privacy' }, api: 'http://override.test/api', privacy: 'http://override.test/privacy', collector: 'http://environment.test' },
|
|
67
|
+
{ name: 'api-url-env', config: {}, env: { REPRO_API_BASE_URL: `${local}/api`, REPRO_API_BASE: 'http://environment.test/api' }, api: `${local}/api`, privacy: `${local}/api`, collector: COLLECTOR },
|
|
68
|
+
{ name: 'privacy-env', config: {}, env: { REPRO_PRIVACY_API_BASE: `${local}/privacy` }, api: API, privacy: `${local}/privacy`, collector: COLLECTOR },
|
|
69
|
+
];
|
|
70
|
+
for (const entry of cases) {
|
|
71
|
+
clearEnv(); Object.assign(process.env, entry.env);
|
|
72
|
+
requests.length = 0;
|
|
73
|
+
const cfg = { tenantId: 'TENANT_test', appId: 'APP_test', appSecret: 'test-only', serviceName: entry.name, ...entry.config };
|
|
74
|
+
await __reproTestHooks.registerRuntimeServiceForTest(cfg);
|
|
75
|
+
assert.equal(requests[0].url, `${entry.api}/v1/sdk/runtime-services`);
|
|
76
|
+
assert.equal(requests[0].options.headers['X-App-Id'], 'APP_test');
|
|
77
|
+
const apiBase = resolvePrivacyApiBase(cfg.privacy, cfg.apiBase);
|
|
78
|
+
assert.equal(apiBase, entry.privacy);
|
|
79
|
+
const policy = await fetchApprovedRuntimePrivacyPolicy({ ...cfg, apiBase, environment: 'dev' });
|
|
80
|
+
assert.equal(policy.status, 'ok');
|
|
81
|
+
assert.equal(new URL(requests[1].url).pathname, new URL(`${entry.privacy}/v1/sdk/privacy-policy`).pathname);
|
|
82
|
+
const sid = `S_${entry.name}`;
|
|
83
|
+
enqueueIngestEntries(cfg, sid, [{ actionId: 'A_test', requestRid: 'R_test', db: [{ collection: 'orders', op: 'findOne', query: { order: entry.name } }], t: Date.now() }]);
|
|
84
|
+
await flushIngestQueue();
|
|
85
|
+
assert.equal(requests.at(-1).url, `${entry.collector}/v1/ingest/events`);
|
|
86
|
+
assert.equal(await events.countDocuments({ session_id: sid, event_type: 'db_change' }), 1);
|
|
87
|
+
}
|
|
88
|
+
assert.deepEqual(errors, []);
|
|
89
|
+
console.log('hosted endpoint defaults, local/env overrides, precedence, policy fetch and persisted ingestion passed (no production traffic)');
|
|
90
|
+
} finally {
|
|
91
|
+
global.fetch = originalFetch;
|
|
92
|
+
clearEnv();
|
|
93
|
+
for (const [key, value] of Object.entries(originalEnv)) if (value !== undefined) process.env[key] = value;
|
|
94
|
+
if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
|
|
95
|
+
await mongo?.close();
|
|
96
|
+
await container?.stop();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|