@zintrust/trace 0.4.94 → 0.4.96

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.
@@ -21,6 +21,58 @@ const describeValueType = (value) => {
21
21
  return 'null';
22
22
  return typeof value;
23
23
  };
24
+ const chooseLargerCandidate = (left, right) => {
25
+ if (left === null)
26
+ return right;
27
+ if (right === null)
28
+ return left;
29
+ return right.size > left.size ? right : left;
30
+ };
31
+ const fallbackCandidate = (value, path) => {
32
+ return path.length === 0 ? null : { path, size: serializedSize(value) };
33
+ };
34
+ const findLargestDroppablePathInArray = (value, path) => {
35
+ let best = null;
36
+ for (const [index, item] of value.entries()) {
37
+ best = chooseLargerCandidate(best, findLargestDroppablePath(item, [...path, index]));
38
+ }
39
+ return best ?? fallbackCandidate(value, path);
40
+ };
41
+ const findLargestDroppablePathInObject = (value, path) => {
42
+ let best = null;
43
+ for (const [key, entryValue] of Object.entries(value)) {
44
+ if (key === '__traceNotice')
45
+ continue;
46
+ best = chooseLargerCandidate(best, findLargestDroppablePath(entryValue, [...path, key]));
47
+ }
48
+ return best ?? fallbackCandidate(value, path);
49
+ };
50
+ const findLargestDroppablePath = (value, path = []) => {
51
+ if (Array.isArray(value))
52
+ return findLargestDroppablePathInArray(value, path);
53
+ if (typeof value === 'object' && value !== null) {
54
+ return findLargestDroppablePathInObject(value, path);
55
+ }
56
+ return fallbackCandidate(value, path);
57
+ };
58
+ const replaceAtPath = (value, path, replacement) => {
59
+ if (path.length === 0)
60
+ return replacement;
61
+ const [segment, ...rest] = path;
62
+ if (Array.isArray(value) && typeof segment === 'number') {
63
+ const next = value.slice();
64
+ next[segment] = replaceAtPath(next[segment], rest, replacement);
65
+ return next;
66
+ }
67
+ if (typeof value === 'object' && value !== null && typeof segment === 'string') {
68
+ const current = value;
69
+ return {
70
+ ...current,
71
+ [segment]: replaceAtPath(current[segment], rest, replacement),
72
+ };
73
+ }
74
+ return value;
75
+ };
24
76
  const compactValue = (value, depth) => {
25
77
  if (depth >= DEFAULT_MAX_DEPTH) {
26
78
  return DROPPED_FIELD_MESSAGE;
@@ -52,18 +104,18 @@ const compactValue = (value, depth) => {
52
104
  }
53
105
  return Object.fromEntries(compactedEntries);
54
106
  };
55
- const compactTopLevelObjectToBudget = (value) => {
56
- const compacted = {
57
- ...value,
58
- __traceNotice: COMPACTED_CONTENT_MESSAGE,
59
- };
60
- const keysByDescendingSize = Object.keys(compacted)
61
- .filter((key) => key !== '__traceNotice')
62
- .sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
63
- for (const key of keysByDescendingSize) {
64
- if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES)
107
+ const compactStructuredValueToBudget = (value) => {
108
+ let compacted = typeof value === 'object' && value !== null && !Array.isArray(value)
109
+ ? {
110
+ ...value,
111
+ __traceNotice: COMPACTED_CONTENT_MESSAGE,
112
+ }
113
+ : value;
114
+ while (serializedSize(compacted) > DEFAULT_MAX_ENTRY_BYTES) {
115
+ const candidate = findLargestDroppablePath(compacted);
116
+ if (candidate === null)
65
117
  break;
66
- compacted[key] = DROPPED_FIELD_MESSAGE;
118
+ compacted = replaceAtPath(compacted, candidate.path, DROPPED_FIELD_MESSAGE);
67
119
  }
68
120
  return compacted;
69
121
  };
@@ -75,10 +127,10 @@ const fitContentToBudget = (content) => {
75
127
  if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
76
128
  return compacted;
77
129
  }
78
- if (typeof compacted === 'object' && compacted !== null && !Array.isArray(compacted)) {
79
- const topLevelCompacted = compactTopLevelObjectToBudget(compacted);
80
- if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
81
- return topLevelCompacted;
130
+ if (typeof compacted === 'object' && compacted !== null) {
131
+ const budgetCompacted = compactStructuredValueToBudget(compacted);
132
+ if (serializedSize(budgetCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
133
+ return budgetCompacted;
82
134
  }
83
135
  }
84
136
  return {
package/dist/types.d.ts CHANGED
@@ -189,6 +189,7 @@ export interface ViewContent {
189
189
  hostname: string;
190
190
  }
191
191
  export interface ClientRequestContent {
192
+ source?: string;
192
193
  method: string;
193
194
  url: string;
194
195
  requestHeaders: Record<string, string>;
@@ -201,6 +202,7 @@ export interface ClientRequestContent {
201
202
  hostname: string;
202
203
  }
203
204
  export interface ClientRequestTraceInput {
205
+ source?: string;
204
206
  method: string;
205
207
  url: string;
206
208
  requestHeaders: Record<string, string>;
@@ -268,6 +270,12 @@ export type TraceFilterRule = {
268
270
  include?: string[];
269
271
  exclude?: string[];
270
272
  };
273
+ export type TraceClientRequestCaptureRule = TraceFilterRule & {
274
+ requestHeaders?: boolean;
275
+ requestBody?: boolean;
276
+ responseHeaders?: boolean;
277
+ responseBody?: boolean;
278
+ };
271
279
  export type TraceRequestWatcherConfig = TraceFilterRule & {
272
280
  all?: TraceFilterRule;
273
281
  get?: TraceFilterRule;
@@ -276,8 +284,12 @@ export type TraceRequestWatcherConfig = TraceFilterRule & {
276
284
  patch?: TraceFilterRule;
277
285
  delete?: TraceFilterRule;
278
286
  };
287
+ export type TraceClientRequestWatcherConfig = TraceClientRequestCaptureRule & {
288
+ sources?: Record<string, TraceClientRequestCaptureRule>;
289
+ };
279
290
  export type TraceWatcherToggle = boolean | TraceFilterRule;
280
291
  export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
292
+ export type TraceClientRequestWatcherToggle = boolean | TraceClientRequestWatcherConfig;
281
293
  export type WatcherToggles = {
282
294
  request?: TraceRequestWatcherToggle;
283
295
  query?: TraceWatcherToggle;
@@ -298,7 +310,7 @@ export type WatcherToggles = {
298
310
  batch?: TraceWatcherToggle;
299
311
  dump?: TraceWatcherToggle;
300
312
  view?: TraceWatcherToggle;
301
- clientRequest?: TraceWatcherToggle;
313
+ clientRequest?: TraceClientRequestWatcherToggle;
302
314
  };
303
315
  export interface ITraceConfig {
304
316
  enabled: boolean;
@@ -13,6 +13,8 @@ const normalizeTerms = (terms) => {
13
13
  const matchesRule = (haystack, rule) => {
14
14
  if (!rule)
15
15
  return true;
16
+ if (rule.enabled === false)
17
+ return false;
16
18
  const include = normalizeTerms(rule.include);
17
19
  const exclude = normalizeTerms(rule.exclude);
18
20
  if (exclude.some((term) => haystack.includes(term)))
@@ -71,6 +73,16 @@ const getRequestMethodRule = (watcher, entry) => {
71
73
  return watcher.delete;
72
74
  return watcher.all;
73
75
  };
76
+ const getClientRequestSourceRule = (watcher, entry) => {
77
+ if (entry.type !== EntryType.CLIENT_REQUEST)
78
+ return undefined;
79
+ const content = isObjectValue(entry.content) ? entry.content : undefined;
80
+ const sourceValue = content?.['source'];
81
+ const source = typeof sourceValue === 'string' ? sourceValue.trim().toLowerCase() : '';
82
+ if (source === '')
83
+ return undefined;
84
+ return watcher.sources?.[source];
85
+ };
74
86
  export const TraceEntryFilter = Object.freeze({
75
87
  shouldCapture(entry, config) {
76
88
  const watcherKey = watcherKeyByEntryType[entry.type];
@@ -90,6 +102,14 @@ export const TraceEntryFilter = Object.freeze({
90
102
  if (!matchesRule(haystack, methodRule))
91
103
  return false;
92
104
  }
105
+ if (watcherKey === 'clientRequest') {
106
+ const clientRequestWatcher = watcher;
107
+ const sourceRule = getClientRequestSourceRule(clientRequestWatcher, entry);
108
+ if (sourceRule?.enabled === false)
109
+ return false;
110
+ if (!matchesRule(haystack, sourceRule))
111
+ return false;
112
+ }
93
113
  return true;
94
114
  },
95
115
  });
@@ -1,5 +1,5 @@
1
1
  import type { ClientRequestTraceInput, ITraceWatcher } from '../types';
2
- declare const emit: ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
2
+ declare const emit: ({ source, method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
3
3
  export declare const HttpClientWatcher: ITraceWatcher & {
4
4
  emit: typeof emit;
5
5
  };
@@ -7,32 +7,104 @@ let _storage = null;
7
7
  let _redactHeaderNames = [];
8
8
  let _redactBodyFields = [];
9
9
  let _ignoreRoutes = [];
10
- const emit = ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }) => {
10
+ let _clientRequestWatcher;
11
+ const isObjectValue = (value) => {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ };
14
+ const resolveSource = (value) => {
15
+ if (typeof value !== 'string')
16
+ return undefined;
17
+ const normalized = value.trim().toLowerCase();
18
+ return normalized === '' ? undefined : normalized;
19
+ };
20
+ const resolveSourceRule = (source) => {
21
+ if (source === undefined)
22
+ return undefined;
23
+ return _clientRequestWatcher?.sources?.[source];
24
+ };
25
+ const shouldCaptureField = (field, sourceRule) => {
26
+ const scoped = sourceRule?.[field];
27
+ if (typeof scoped === 'boolean')
28
+ return scoped;
29
+ const global = _clientRequestWatcher?.[field];
30
+ if (typeof global === 'boolean')
31
+ return global;
32
+ return true;
33
+ };
34
+ const buildRequestHeaders = (requestHeaders, sourceRule) => {
35
+ return shouldCaptureField('requestHeaders', sourceRule)
36
+ ? { requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames) }
37
+ : { requestHeaders: {} };
38
+ };
39
+ const buildRequestBody = (requestBody, sourceRule) => {
40
+ if (requestBody === undefined)
41
+ return {};
42
+ if (!shouldCaptureField('requestBody', sourceRule))
43
+ return {};
44
+ return { requestBody: redactUnknown(requestBody, _redactBodyFields) };
45
+ };
46
+ const buildResponseHeaders = (responseHeaders, sourceRule) => {
47
+ if (responseHeaders === undefined)
48
+ return {};
49
+ if (!shouldCaptureField('responseHeaders', sourceRule))
50
+ return {};
51
+ return { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) };
52
+ };
53
+ const buildResponseBody = (responseBody, sourceRule) => {
54
+ if (responseBody === undefined)
55
+ return {};
56
+ if (!shouldCaptureField('responseBody', sourceRule))
57
+ return {};
58
+ return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
59
+ };
60
+ const buildClientRequestContent = (input, sourceRule, normalizedSource) => {
61
+ return {
62
+ ...(normalizedSource === undefined ? {} : { source: normalizedSource }),
63
+ method: input.method.toUpperCase(),
64
+ url: input.url,
65
+ ...buildRequestHeaders(input.requestHeaders, sourceRule),
66
+ ...buildRequestBody(input.requestBody, sourceRule),
67
+ ...(input.responseStatus === undefined ? {} : { responseStatus: input.responseStatus }),
68
+ ...buildResponseHeaders(input.responseHeaders, sourceRule),
69
+ ...buildResponseBody(input.responseBody, sourceRule),
70
+ ...(typeof input.error === 'string' && input.error !== '' ? { error: input.error } : {}),
71
+ duration: input.duration,
72
+ hostname: TraceContext.getHostname(),
73
+ };
74
+ };
75
+ const isWatcherEnabled = (value) => {
76
+ if (value === false)
77
+ return false;
78
+ if (isObjectValue(value) && value.enabled === false)
79
+ return false;
80
+ return true;
81
+ };
82
+ const emit = ({ source, method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }) => {
11
83
  if (!_storage)
12
84
  return;
13
85
  if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
14
86
  return;
87
+ const normalizedSource = resolveSource(source);
88
+ const sourceRule = resolveSourceRule(normalizedSource);
89
+ if (sourceRule?.enabled === false)
90
+ return;
15
91
  const tags = AuthTag.append([method.toUpperCase()]);
16
92
  if ((responseStatus ?? 0) >= 400 || error)
17
93
  tags.push('failed');
18
- const content = {
19
- method: method.toUpperCase(),
94
+ if (normalizedSource !== undefined)
95
+ tags.push(normalizedSource);
96
+ const content = buildClientRequestContent({
97
+ source,
98
+ method,
20
99
  url,
21
- requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
22
- ...(requestBody === undefined
23
- ? {}
24
- : { requestBody: redactUnknown(requestBody, _redactBodyFields) }),
25
- ...(responseStatus === undefined ? {} : { responseStatus }),
26
- ...(responseHeaders === undefined
27
- ? {}
28
- : { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) }),
29
- ...(responseBody === undefined
30
- ? {}
31
- : { responseBody: redactUnknown(responseBody, _redactBodyFields) }),
32
- ...(typeof error === 'string' && error !== '' ? { error } : {}),
100
+ requestHeaders,
101
+ responseStatus,
33
102
  duration,
34
- hostname: TraceContext.getHostname(),
35
- };
103
+ requestBody,
104
+ responseHeaders,
105
+ responseBody,
106
+ error,
107
+ }, sourceRule, normalizedSource);
36
108
  _storage
37
109
  .writeEntry({
38
110
  uuid: crypto.randomUUID(),
@@ -48,14 +120,19 @@ const emit = ({ method, url, requestHeaders, responseStatus, duration, requestBo
48
120
  export const HttpClientWatcher = Object.freeze({
49
121
  emit,
50
122
  register({ storage, config }) {
51
- if (config.watchers.clientRequest === false)
123
+ if (!isWatcherEnabled(config.watchers.clientRequest))
52
124
  return () => undefined;
53
125
  _storage = storage;
126
+ _clientRequestWatcher =
127
+ typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
128
+ ? config.watchers.clientRequest
129
+ : undefined;
54
130
  _redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
55
131
  _redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
56
132
  _ignoreRoutes = config.ignoreRoutes;
57
133
  return () => {
58
134
  _storage = null;
135
+ _clientRequestWatcher = undefined;
59
136
  _redactBodyFields = [];
60
137
  _ignoreRoutes = [];
61
138
  };
@@ -17,6 +17,12 @@ const normalizeHeaders = (headers) => {
17
17
  const normalizeHeaderValue = (value) => {
18
18
  return Array.isArray(value) ? value.join(', ') : value;
19
19
  };
20
+ const resolveRouteMiddleware = (req) => {
21
+ const middleware = req.context?.['traceRouteMiddleware'];
22
+ return Array.isArray(middleware)
23
+ ? middleware.filter((value) => typeof value === 'string')
24
+ : [];
25
+ };
20
26
  const resolveRequestPayload = (req, config) => {
21
27
  const redactFields = [...config.redaction.keys, ...config.redaction.body];
22
28
  const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
@@ -108,7 +114,7 @@ const buildEntry = (req, res, start, config, responseCapture) => {
108
114
  responseBody: responseCapture.body,
109
115
  duration: Date.now() - start,
110
116
  memory: TraceContext.getMemory(),
111
- middleware: [],
117
+ middleware: resolveRouteMiddleware(req),
112
118
  hostname: TraceContext.getHostname(),
113
119
  userId: TraceContext.getUserId(),
114
120
  };
@@ -33,7 +33,12 @@ export const MiddlewareWatcher = Object.freeze({
33
33
  return () => undefined;
34
34
  _storage = storage;
35
35
  _ignoreRoutes = config.ignoreRoutes;
36
+ globalThis.__zintrust_trace_middleware_emit__ = emit;
36
37
  return () => {
38
+ const globalState = globalThis;
39
+ if (globalState.__zintrust_trace_middleware_emit__ === emit) {
40
+ delete globalState.__zintrust_trace_middleware_emit__;
41
+ }
37
42
  _storage = null;
38
43
  _ignoreRoutes = [];
39
44
  };
@@ -34,7 +34,12 @@ export const ModelWatcher = Object.freeze({
34
34
  return () => undefined;
35
35
  _storage = storage;
36
36
  _ignoreRoutes = config.ignoreRoutes;
37
+ globalThis.__zintrust_trace_model_emit__ = emit;
37
38
  return () => {
39
+ const globalState = globalThis;
40
+ if (globalState.__zintrust_trace_model_emit__ === emit) {
41
+ delete globalState.__zintrust_trace_model_emit__;
42
+ }
38
43
  _storage = null;
39
44
  _ignoreRoutes = [];
40
45
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.94",
3
+ "version": "0.4.96",
4
4
  "description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zintrust/core": "^0.4.93"
43
+ "@zintrust/core": "^0.4.95"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/config.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  import type {
5
5
  ITraceConfig,
6
+ TraceClientRequestCaptureRule,
7
+ TraceClientRequestWatcherToggle,
6
8
  TraceConfigOverrides,
7
9
  TraceFilterRule,
8
10
  TraceRequestWatcherConfig,
@@ -25,19 +27,44 @@ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
25
27
  return typeof value === 'object' && value !== null && !Array.isArray(value);
26
28
  };
27
29
 
30
+ const resolveEnabled = (
31
+ base?: TraceFilterRule,
32
+ override?: TraceFilterRule
33
+ ): boolean | undefined => {
34
+ return override?.enabled ?? base?.enabled;
35
+ };
36
+
37
+ const hasMergedRuleValues = (
38
+ include: string[],
39
+ exclude: string[],
40
+ enabled: boolean | undefined
41
+ ): boolean => {
42
+ return include.length > 0 || exclude.length > 0 || enabled !== undefined;
43
+ };
44
+
45
+ const buildFilterRule = (input: {
46
+ include: string[];
47
+ exclude: string[];
48
+ enabled: boolean | undefined;
49
+ }): TraceFilterRule => {
50
+ return Object.freeze({
51
+ ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
52
+ ...(input.include.length > 0 ? { include: input.include } : {}),
53
+ ...(input.exclude.length > 0 ? { exclude: input.exclude } : {}),
54
+ });
55
+ };
56
+
28
57
  const mergeFilterRule = (
29
58
  base?: TraceFilterRule,
30
59
  override?: TraceFilterRule
31
60
  ): TraceFilterRule | undefined => {
32
61
  const include = mergeStringLists(base?.include ?? [], override?.include);
33
62
  const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
63
+ const enabled = resolveEnabled(base, override);
34
64
 
35
- if (include.length === 0 && exclude.length === 0) return undefined;
65
+ if (!hasMergedRuleValues(include, exclude, enabled)) return undefined;
36
66
 
37
- return Object.freeze({
38
- ...(include.length > 0 ? { include } : {}),
39
- ...(exclude.length > 0 ? { exclude } : {}),
40
- });
67
+ return buildFilterRule({ include, exclude, enabled });
41
68
  };
42
69
 
43
70
  const mergeWatcherToggle = (
@@ -51,6 +78,109 @@ const mergeWatcherToggle = (
51
78
  return mergeFilterRule(baseRule, override);
52
79
  };
53
80
 
81
+ type ClientRequestCaptureFlags = Pick<
82
+ TraceClientRequestCaptureRule,
83
+ 'requestHeaders' | 'requestBody' | 'responseHeaders' | 'responseBody'
84
+ >;
85
+
86
+ const resolveClientRequestCaptureFlags = (
87
+ base?: TraceClientRequestCaptureRule,
88
+ override?: TraceClientRequestCaptureRule
89
+ ): ClientRequestCaptureFlags => {
90
+ return {
91
+ requestHeaders: override?.requestHeaders ?? base?.requestHeaders,
92
+ requestBody: override?.requestBody ?? base?.requestBody,
93
+ responseHeaders: override?.responseHeaders ?? base?.responseHeaders,
94
+ responseBody: override?.responseBody ?? base?.responseBody,
95
+ };
96
+ };
97
+
98
+ const hasClientRequestCaptureFlags = (flags: ClientRequestCaptureFlags): boolean => {
99
+ return Object.values(flags).some((value) => value !== undefined);
100
+ };
101
+
102
+ const buildClientRequestCaptureRule = (
103
+ mergedRule: TraceFilterRule | undefined,
104
+ flags: ClientRequestCaptureFlags
105
+ ): TraceClientRequestCaptureRule => {
106
+ const baseRule = mergedRule ? { ...mergedRule } : {};
107
+
108
+ return Object.freeze({
109
+ ...baseRule,
110
+ ...(flags.requestHeaders === undefined ? {} : { requestHeaders: flags.requestHeaders }),
111
+ ...(flags.requestBody === undefined ? {} : { requestBody: flags.requestBody }),
112
+ ...(flags.responseHeaders === undefined ? {} : { responseHeaders: flags.responseHeaders }),
113
+ ...(flags.responseBody === undefined ? {} : { responseBody: flags.responseBody }),
114
+ });
115
+ };
116
+
117
+ const mergeClientRequestCaptureRule = (
118
+ base?: TraceClientRequestCaptureRule,
119
+ override?: TraceClientRequestCaptureRule
120
+ ): TraceClientRequestCaptureRule | undefined => {
121
+ const mergedRule = mergeFilterRule(base, override);
122
+ const flags = resolveClientRequestCaptureFlags(base, override);
123
+
124
+ if (mergedRule === undefined && !hasClientRequestCaptureFlags(flags)) {
125
+ return undefined;
126
+ }
127
+
128
+ return buildClientRequestCaptureRule(mergedRule, flags);
129
+ };
130
+
131
+ const collectClientRequestSourceKeys = (
132
+ base?: TraceClientRequestWatcherToggle,
133
+ override?: Exclude<TraceClientRequestWatcherToggle, boolean>
134
+ ): string[] => {
135
+ const overrideSources = override?.sources ?? {};
136
+ const sourceKeys = new Set<string>([
137
+ ...Object.keys(isObjectValue(base) ? base.sources ?? {} : {}),
138
+ ...Object.keys(overrideSources),
139
+ ]);
140
+
141
+ return [...sourceKeys];
142
+ };
143
+
144
+ const mergeClientRequestSources = (
145
+ base?: TraceClientRequestWatcherToggle,
146
+ override?: Exclude<TraceClientRequestWatcherToggle, boolean>
147
+ ): Record<string, TraceClientRequestCaptureRule> | undefined => {
148
+ if (override === undefined) return undefined;
149
+
150
+ const sources: Record<string, TraceClientRequestCaptureRule> = {};
151
+
152
+ for (const key of collectClientRequestSourceKeys(base, override)) {
153
+ const baseSources = isObjectValue(base) ? base.sources : undefined;
154
+ const sourceRule = mergeClientRequestCaptureRule(baseSources?.[key], override.sources?.[key]);
155
+ if (sourceRule !== undefined) {
156
+ sources[key] = sourceRule;
157
+ }
158
+ }
159
+
160
+ return Object.keys(sources).length === 0 ? undefined : sources;
161
+ };
162
+
163
+ const mergeClientRequestWatcherToggle = (
164
+ base?: TraceClientRequestWatcherToggle,
165
+ override?: TraceClientRequestWatcherToggle
166
+ ): TraceClientRequestWatcherToggle | undefined => {
167
+ if (override === undefined) return base;
168
+ if (override === false || override === true) return override;
169
+
170
+ const baseConfig = isObjectValue(base) ? base : undefined;
171
+ const merged = mergeClientRequestCaptureRule(baseConfig, override) ?? {};
172
+ const sources = mergeClientRequestSources(base, override);
173
+
174
+ if (sources === undefined) {
175
+ return merged;
176
+ }
177
+
178
+ return Object.freeze({
179
+ ...merged,
180
+ sources,
181
+ });
182
+ };
183
+
54
184
  const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'] as const;
55
185
 
56
186
  const mergeRequestWatcherToggle = (
@@ -99,7 +229,7 @@ const mergeWatchers = (
99
229
  batch: mergeWatcherToggle(base.batch, override.batch),
100
230
  dump: mergeWatcherToggle(base.dump, override.dump),
101
231
  view: mergeWatcherToggle(base.view, override.view),
102
- clientRequest: mergeWatcherToggle(base.clientRequest, override.clientRequest),
232
+ clientRequest: mergeClientRequestWatcherToggle(base.clientRequest, override.clientRequest),
103
233
  };
104
234
  };
105
235