Skip to content

Commit 21cdc9b

Browse files
committed
Get rid of 500ms timeout
Replaces the hardcoded FIXME sleep(500ms) timeout in each test. Instead, we track any pending `PerformanceObserver`s and wait for them to disconnect before continuing. This doesn't magically make the tests run faster, in a way it just hides the timeout, because the implementation actually still waits for `OBSERVER_WAIT_TIME_MS` (300ms) internally before disconnecting the PO. However, this makes the tests follow the actual real world behavior and they are only as slow as the actual behavior IRL. IMO, the timeout isn't actually necessary IF we have PO available, and that can be removed in a future refactor. When we address that, most of the tests will automatically run faster.
1 parent 9f8149c commit 21cdc9b

File tree

1 file changed

+143
-11
lines changed
  • experimental/packages/opentelemetry-instrumentation-fetch/test

1 file changed

+143
-11
lines changed

experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts

+143-11
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ import {
5555
import * as msw from 'msw';
5656
import { setupWorker } from 'msw/browser';
5757

58+
// This should match the unexported constant with the same name in fetch.ts
59+
const OBSERVER_WAIT_TIME_MS = 300;
60+
5861
class DummySpanExporter implements tracing.SpanExporter {
5962
readonly exported: tracing.ReadableSpan[][] = [];
6063

@@ -91,6 +94,34 @@ const ORIGIN = location.origin;
9194
// "localhost:9876"
9295
const ORIGIN_HOST = new URL(ORIGIN).host;
9396

97+
interface Resolvers<T> {
98+
promise: Promise<T>;
99+
resolve: (value: T) => void;
100+
reject: (reason: any) => void;
101+
}
102+
103+
// Use Promise.withResolvers when we can
104+
function withResolvers<T>(): Resolvers<T> {
105+
let resolve: (value: T) => void;
106+
let reject: (reason: any) => void;
107+
let promise = new Promise<T>((res, rej) => {
108+
resolve = res;
109+
reject = rej;
110+
});
111+
112+
return {
113+
promise,
114+
resolve: resolve!,
115+
reject: reject!,
116+
};
117+
}
118+
119+
function waitFor(timeout: number): Promise<void> {
120+
return new Promise(resolve => {
121+
setTimeout(resolve, timeout);
122+
});
123+
}
124+
94125
describe('fetch', () => {
95126
let workerStarted = false;
96127

@@ -105,13 +136,74 @@ describe('fetch', () => {
105136
workerStarted = true;
106137
};
107138

108-
afterEach(() => {
109-
if (workerStarted) {
110-
worker.stop();
111-
workerStarted = false;
139+
let pendingObservers = 0;
140+
let waitForPerformanceObservers = async () => {};
141+
142+
beforeEach(() => {
143+
if (PerformanceObserver) {
144+
assert.strictEqual(
145+
pendingObservers,
146+
0,
147+
'Did a previous test leak a PerformanceObserver?'
148+
);
149+
150+
let resolvers: Resolvers<void> | undefined;
151+
152+
let _observe = PerformanceObserver.prototype.observe;
153+
let _disconnect = PerformanceObserver.prototype.disconnect;
154+
155+
sinon.stub(PerformanceObserver.prototype, 'observe').callsFake(function (
156+
this: PerformanceObserver,
157+
...args
158+
) {
159+
_observe.call(this, ...args);
160+
pendingObservers++;
161+
162+
if (!resolvers) {
163+
resolvers = withResolvers();
164+
}
165+
});
166+
167+
sinon
168+
.stub(PerformanceObserver.prototype, 'disconnect')
169+
.callsFake(function (this: PerformanceObserver, ...args) {
170+
_disconnect.call(this, ...args);
171+
pendingObservers--;
172+
173+
if (pendingObservers === 0) {
174+
resolvers?.resolve();
175+
resolvers = undefined;
176+
}
177+
});
178+
179+
waitForPerformanceObservers = async (): Promise<void> => {
180+
while (resolvers) {
181+
await resolvers.promise;
182+
}
183+
};
112184
}
185+
});
186+
187+
afterEach(() => {
188+
try {
189+
if (workerStarted) {
190+
worker.stop();
191+
workerStarted = false;
192+
}
193+
194+
const _pendingObservers = pendingObservers;
113195

114-
sinon.restore();
196+
pendingObservers = 0;
197+
waitForPerformanceObservers = async () => {};
198+
199+
assert.strictEqual(
200+
_pendingObservers,
201+
0,
202+
`Test leaked ${_pendingObservers} \`PerformanceObserver\`(s)!`
203+
);
204+
} finally {
205+
sinon.restore();
206+
}
115207
});
116208

117209
describe('enabling/disabling', () => {
@@ -178,10 +270,7 @@ describe('fetch', () => {
178270
callback
179271
);
180272

181-
// FIXME!
182-
await new Promise(resolve => {
183-
setTimeout(resolve, 500);
184-
});
273+
await waitForPerformanceObservers();
185274

186275
if (expectExport) {
187276
// This isn't intended to be an invariant, but in the current setup we
@@ -1594,7 +1683,28 @@ describe('fetch', () => {
15941683

15951684
getEntriesByTypeSpy = sinon.spy(performance, 'getEntriesByType');
15961685

1597-
const result = await tracedFetch();
1686+
const result = await tracedFetch({
1687+
callback: async () => {
1688+
const response = await fetch('/api/status.json');
1689+
1690+
// This seemingly random timeout is testing real behavior!
1691+
//
1692+
// Currently, the implementation works by waiting a hardcoded
1693+
// OBSERVER_WAIT_TIME_MS before trying to get the resource
1694+
// timing entries, and hoping that they are there by then.
1695+
//
1696+
// We will match that here plus an additional 50ms. If the
1697+
// tests still fail despite this timeout, then we may have
1698+
// found a bug that could occur in the real world, and it's
1699+
// probably time to revisit the naïve implementation.
1700+
//
1701+
// This should be updated as the implementation changes.
1702+
await waitFor(OBSERVER_WAIT_TIME_MS + 50);
1703+
1704+
return response;
1705+
},
1706+
});
1707+
15981708
rootSpan = result.rootSpan;
15991709
});
16001710

@@ -1659,7 +1769,29 @@ describe('fetch', () => {
16591769
beforeEach(async () => {
16601770
sinon.stub(performance, 'getEntriesByType').value(undefined);
16611771

1662-
const result = await tracedFetch();
1772+
const result = await tracedFetch({
1773+
callback: async () => {
1774+
const response = await fetch('/api/status.json');
1775+
1776+
// Currently, the implementation works by waiting a hardcoded
1777+
// OBSERVER_WAIT_TIME_MS before closing out the span.
1778+
//
1779+
// This is really meant for the case where we use the
1780+
// `getEntriesByType` fallback, but the current implementation
1781+
// waits for that timeout unconditionally. That is a bit silly,
1782+
// but on the other hand, it's unclear if anyone out there is
1783+
// actually running this in an environment where `fetch()` is
1784+
// available but `getEntriesByType` is not.
1785+
//
1786+
// For now, we will match that here plus an additional 50ms.
1787+
// If we refactor the implementation to do without the timeout
1788+
// in this case, this can be safely deleted.
1789+
await waitFor(OBSERVER_WAIT_TIME_MS + 50);
1790+
1791+
return response;
1792+
},
1793+
});
1794+
16631795
rootSpan = result.rootSpan;
16641796
});
16651797

0 commit comments

Comments
 (0)