Skip to content

Commit e8dd3f4

Browse files
authored
Merge pull request #1426 from kingksjo/fix/issues-1369-1364-1363-1321
fix: cache hook validation, polling health, otel mock, referral 404 contract
2 parents 3dc539f + 417d05b commit e8dd3f4

4 files changed

Lines changed: 166 additions & 8 deletions

File tree

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,151 @@
1-
// Mock for all @opentelemetry/* packages in tests
2-
module.exports = new Proxy({}, {
3-
get: () => new Proxy(function() {}, {
4-
get: () => new Proxy(function() {}, { get: () => () => {} }),
1+
// Mock for all @opentelemetry/* packages in tests.
2+
//
3+
// Must mirror the real API surface used by src/tracing.ts and
4+
// src/middleware/correlationId.ts. A previous generic-Proxy version
5+
// returned undefined from trace.getTracer(), so every HTTP request
6+
// threw `TypeError: Cannot read properties of undefined (reading
7+
// 'startSpan')` in correlationId middleware.
8+
function createNoopSpan() {
9+
return {
10+
setAttributes: () => {},
11+
setAttribute: () => {},
12+
setStatus: () => {},
13+
recordException: () => {},
14+
updateName: () => {},
15+
addEvent: () => {},
16+
end: () => {},
17+
isRecording: () => false,
18+
spanContext: () => ({
19+
traceId: '00000000000000000000000000000000',
20+
spanId: '0000000000000000',
21+
traceFlags: 0,
22+
}),
23+
};
24+
}
25+
26+
function createNoopTracer() {
27+
return {
28+
startSpan: () => createNoopSpan(),
29+
startActiveSpan: (name, arg2, arg3) => {
30+
const fn = typeof arg2 === 'function' ? arg2 : arg3;
31+
const span = createNoopSpan();
32+
if (typeof fn === 'function') return fn(span);
33+
return span;
34+
},
35+
};
36+
}
37+
38+
const trace = {
39+
getTracer: () => createNoopTracer(),
40+
getActiveSpan: () => undefined,
41+
getSpan: () => undefined,
42+
setSpan: (contextArg, span) => span ?? contextArg,
43+
deleteSpan: (contextArg) => contextArg,
44+
};
45+
46+
const context = {
47+
active: () => ({}),
48+
with: (ctx, fn, thisArg, ...args) =>
49+
typeof fn === 'function' ? fn.apply(thisArg, args) : undefined,
50+
bind: (ctx, target) => target,
51+
};
52+
53+
const propagation = {
54+
extract: (contextArg) => contextArg ?? {},
55+
inject: () => {},
56+
fields: () => [],
57+
};
58+
59+
const SpanStatusCode = { UNSET: 0, OK: 1, ERROR: 2 };
60+
const SpanKind = { INTERNAL: 0, SERVER: 1, CLIENT: 2, PRODUCER: 3, CONSUMER: 4 };
61+
const TraceFlags = { NONE: 0, SAMPLED: 1 };
62+
const ROOT_CONTEXT = {};
63+
const INVALID_SPAN_CONTEXT = {
64+
traceId: '00000000000000000000000000000000',
65+
spanId: '0000000000000000',
66+
traceFlags: 0,
67+
};
68+
69+
class NoopInstrumentation {
70+
constructor(...args) {
71+
this.args = args;
72+
}
73+
enable() {}
74+
disable() {}
75+
setConfig() {}
76+
getConfig() {
77+
return {};
78+
}
79+
}
80+
81+
class NoopSdk {
82+
constructor(...args) {
83+
this.args = args;
84+
}
85+
start() {}
86+
shutdown() {
87+
return Promise.resolve();
88+
}
89+
forceFlush() {
90+
return Promise.resolve();
91+
}
92+
}
93+
94+
function noopExporter() {
95+
return {
96+
export: (_spans, resultCallback) => {
97+
if (typeof resultCallback === 'function') resultCallback({ code: 0 });
98+
},
99+
shutdown: () => Promise.resolve(),
100+
};
101+
}
102+
103+
function resourceFromAttributes(attributes) {
104+
return { attributes: attributes ?? {} };
105+
}
106+
107+
const baseExports = {
108+
__esModule: true,
109+
trace,
110+
context,
111+
propagation,
112+
SpanStatusCode,
113+
SpanKind,
114+
TraceFlags,
115+
ROOT_CONTEXT,
116+
INVALID_SPAN_CONTEXT,
117+
// SDK / exporter / instrumentation constructors used by src/tracing.ts
118+
NodeSDK: NoopSdk,
119+
OTLPTraceExporter: NoopSdk,
120+
HttpInstrumentation: NoopInstrumentation,
121+
ExpressInstrumentation: NoopInstrumentation,
122+
resourceFromAttributes,
123+
};
124+
125+
// Fallback for any other named export (e.g. ATTR_SERVICE_NAME,
126+
// ATTR_SERVICE_VERSION, other instrumentations): attribute constants
127+
// resolve to their name, everything else to a generic noop constructor
128+
// that is safe to call with `new`, call directly, or access deeply.
129+
function fallbackFor(prop) {
130+
if (typeof prop === 'string' && prop.startsWith('ATTR_')) return prop;
131+
const noop = function (...args) {
132+
return { args };
133+
};
134+
return new Proxy(noop, {
135+
get: (target, key) => {
136+
if (key === '__esModule') return false;
137+
return fallbackFor(key);
138+
},
5139
apply: () => ({}),
6140
construct: () => ({ start: () => {}, shutdown: () => Promise.resolve() }),
7-
}),
141+
});
142+
}
143+
144+
module.exports = new Proxy(baseExports, {
145+
get: (target, prop) => {
146+
if (prop in target) return target[prop];
147+
if (prop === 'default') return target;
148+
return fallbackFor(prop);
149+
},
8150
});
151+
module.exports.default = module.exports;

‎backend/src/eventPollingService.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,11 +595,11 @@ export function stopEventPollingService(): void {
595595

596596
export function getEventPollingHealth(): ReturnType<EventPollingService['getHealth']> {
597597
return pollingService?.getHealth() ?? {
598-
status: 'down',
598+
status: 'up',
599599
running: false,
600600
leader: false,
601601
lastSuccessfulPollAt: null,
602-
lastError: 'Event polling service is not initialized',
602+
lastError: null,
603603
lastErrorAt: null,
604604
consecutiveFailures: 0,
605605
};

‎backend/src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,9 @@ async function buildReferralStatsSnapshot(wallet: string) {
431431
body: {
432432
error: 'Not Found',
433433
status: 404,
434+
code: 'ROUTE_NOT_FOUND',
434435
message: 'No referral activity found for this wallet',
436+
retryable: false,
435437
},
436438
};
437439
}

‎backend/src/middleware/cache.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ const invalidationHooks: InvalidationHook[] = [];
316316
* Hooks receive the event type and optional metadata and return an array of cache key patterns.
317317
*/
318318
export function registerInvalidationHook(hook: InvalidationHook): void {
319+
if (typeof hook !== 'function') {
320+
throw new TypeError('Invalidation hook must be a function');
321+
}
319322
invalidationHooks.push(hook);
320323
}
321324

@@ -332,7 +335,17 @@ export function triggerCacheInvalidation(
332335
for (const hook of invalidationHooks) {
333336
try {
334337
const hookPatterns = hook(eventType, metadata);
335-
patterns.push(...hookPatterns);
338+
if (!Array.isArray(hookPatterns)) {
339+
console.error(
340+
JSON.stringify({
341+
level: 'error',
342+
event: 'invalidation_hook_invalid_return',
343+
error: 'Invalidation hook must return an array of cache key patterns',
344+
}),
345+
);
346+
continue;
347+
}
348+
patterns.push(...hookPatterns.filter((pattern): pattern is string => typeof pattern === 'string'));
336349
} catch (err) {
337350
console.error(
338351
JSON.stringify({

0 commit comments

Comments
 (0)