@reproapp/node-sdk 0.0.57 → 0.0.59

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 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. Used by backend live sessions and privacy policy fetches.
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,2 @@
1
+ export declare const DEFAULT_API_BASE = "https://repro-api-d7288.ondigitalocean.app/api";
2
+ export declare const DEFAULT_INGEST_BASE = "https://log-collector-5rj86.ondigitalocean.app";
@@ -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 used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
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
- null;
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
  }
@@ -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 DEFAULT_INGEST_BASE = 'http://localhost:8080';
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}`;
@@ -4,6 +4,7 @@ export interface IngestClientConfig {
4
4
  appSecret?: string;
5
5
  appName?: string;
6
6
  serviceName?: string;
7
+ /** Defaults to the hosted Repro collector; REPRO_INGEST_BASE overrides this value. */
7
8
  ingestBase?: string;
8
9
  ingestPath?: string;
9
10
  actorId?: string;
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
- null;
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.57",
3
+ "version": "0.0.59",
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",
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_API_BASE = 'https://repro-api-d7288.ondigitalocean.app/api';
2
+ export const DEFAULT_INGEST_BASE = 'https://log-collector-5rj86.ondigitalocean.app';
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
- null;
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 used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
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;
@@ -1,7 +1,7 @@
1
1
  import { mapLegacyEntriesToCanonicalEvents } from './mapper';
2
2
  import type { IngestClientConfig, LegacyEntry } from './types';
3
3
 
4
- const DEFAULT_INGEST_BASE = 'http://localhost:8080';
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);
@@ -4,6 +4,7 @@ export interface IngestClientConfig {
4
4
  appSecret?: string;
5
5
  appName?: string;
6
6
  serviceName?: string;
7
+ /** Defaults to the hosted Repro collector; REPRO_INGEST_BASE overrides this value. */
7
8
  ingestBase?: string;
8
9
  ingestPath?: string;
9
10
  actorId?: string;
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
- null;
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; });
@@ -126,7 +126,6 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
126
126
  mapOriginalPosition = sourceInfo.mapOriginalPosition;
127
127
  }
128
128
  if (shouldHandle(filename, metaFilename)) {
129
- try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
130
129
  // Transform the already-compiled JS (keeps Nest’s decorator metadata intact).
131
130
  const res = babel.transformSync(code, {
132
131
  filename,
@@ -161,7 +160,6 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
161
160
  }
162
161
  } catch (err) {
163
162
  out = code; // never break the app if transform fails
164
- try { process.stderr.write(`[trace-debug] compile error: ${filename} :: ${err?.message || err}\n`); } catch {}
165
163
  }
166
164
 
167
165
  const ret = origCompile.call(this, out, filename);