Skip to content

Commit 51960ac

Browse files
e2e tests
1 parent 503ba9f commit 51960ac

26 files changed

Lines changed: 1701 additions & 14 deletions

.github/workflows/e2e.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,20 @@ jobs:
2121
matrix:
2222
library:
2323
# Exclude upstash-redis-js for now because takes ~15 min. To re-enable when someone needs it.
24-
[fetch, firestore, grpc, http, ioredis, mysql, mysql2, nextjs, pg, postgres, prisma]
24+
[
25+
fetch,
26+
firestore,
27+
grpc,
28+
http,
29+
ioredis,
30+
mysql,
31+
mysql2,
32+
nextjs,
33+
pg,
34+
postgres,
35+
prisma,
36+
mongodb,
37+
]
2538
steps:
2639
- name: Checkout
2740
uses: actions/checkout@v4

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Tusk Drift currently supports the following packages and versions:
4848
- **Firestore**: `@google-cloud/firestore@7.x-8.x`
4949
- **Postgres**: `postgres@3.x`
5050
- **MySQL**: `mysql2@3.x`, `mysql@2.x`
51+
- **MongoDB**: `mongodb@5.x-7.x`
5152
- **IORedis**: `ioredis@4.x-5.x`
5253
- **Upstash Redis**: `@upstash/redis@1.x`
5354
- **GraphQL**: `graphql@15.x-16.x`

src/instrumentation/libraries/mongodb/Instrumentation.ts

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@ import {
2121
import { TdSpanAttributes } from "../../../core/types";
2222
import { ConnectionHandler } from "./handlers/ConnectionHandler";
2323
import { TdFakeFindCursor, TdFakeAggregationCursor, TdFakeChangeStream } from "./mocks/FakeCursor";
24+
import { TdFakeTopology } from "./mocks/FakeTopology";
2425
import {
2526
sanitizeBsonValue,
2627
reconstructBsonValue,
2728
addOutputAttributesToSpan,
2829
sanitizeOptions,
30+
wrapCursorOutput,
31+
unwrapCursorOutput,
32+
wrapDirectOutput,
33+
unwrapDirectOutput,
2934
} from "./utils/bsonConversion";
3035

3136
/**
@@ -191,6 +196,18 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
191196
);
192197
}
193198

199+
// Patch Collection.prototype.initializeOrderedBulkOp / initializeUnorderedBulkOp
200+
// In replay mode, inject FakeTopology before calling original to prevent
201+
// "MongoClient must be connected" error from BulkOperationBase constructor.
202+
try {
203+
this._patchBulkOpInitMethods(actualExports);
204+
} catch (error) {
205+
logger.error(
206+
`[${this.INSTRUMENTATION_NAME}] Error patching bulk op init methods, skipping:`,
207+
error,
208+
);
209+
}
210+
194211
// Patch Db.prototype methods
195212
try {
196213
this._patchDbMethods(actualExports);
@@ -345,7 +362,7 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
345362
return resultPromise
346363
.then((result: any) => {
347364
try {
348-
addOutputAttributesToSpan(spanInfo, result);
365+
addOutputAttributesToSpan(spanInfo, wrapDirectOutput(result));
349366
SpanUtils.endSpan(spanInfo.span, {
350367
code: SpanStatusCode.OK,
351368
});
@@ -437,7 +454,9 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
437454
throw new Error(errorMsg);
438455
}
439456

440-
const result = reconstructBsonValue(mockData.result, this.moduleExports);
457+
const result = unwrapDirectOutput(
458+
reconstructBsonValue(mockData.result, this.moduleExports),
459+
);
441460

442461
SpanUtils.endSpan(spanInfo.span, { code: SpanStatusCode.OK });
443462
return result;
@@ -597,7 +616,10 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
597616
if (cursorState.recorded || !cursorState.spanInfo) return;
598617
cursorState.recorded = true;
599618
try {
600-
addOutputAttributesToSpan(cursorState.spanInfo, cursorState.collectedDocuments);
619+
addOutputAttributesToSpan(
620+
cursorState.spanInfo,
621+
wrapCursorOutput(cursorState.collectedDocuments),
622+
);
601623
SpanUtils.endSpan(cursorState.spanInfo.span, {
602624
code: SpanStatusCode.OK,
603625
});
@@ -704,7 +726,7 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
704726
return originalToArray()
705727
.then((result: any[]) => {
706728
try {
707-
addOutputAttributesToSpan(spanInfo, result);
729+
addOutputAttributesToSpan(spanInfo, wrapCursorOutput(result));
708730
SpanUtils.endSpan(spanInfo.span, {
709731
code: SpanStatusCode.OK,
710732
});
@@ -835,7 +857,7 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
835857
return originalForEach(wrappedIterator)
836858
.then(() => {
837859
try {
838-
addOutputAttributesToSpan(spanInfo, collectedDocs);
860+
addOutputAttributesToSpan(spanInfo, wrapCursorOutput(collectedDocs));
839861
SpanUtils.endSpan(spanInfo.span, {
840862
code: SpanStatusCode.OK,
841863
});
@@ -960,12 +982,13 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
960982
throw new Error(errorMsg);
961983
}
962984

963-
const documents = reconstructBsonValue(mockData.result, self.moduleExports);
985+
const reconstructed = reconstructBsonValue(mockData.result, self.moduleExports);
986+
const documents = unwrapCursorOutput(reconstructed);
964987

965988
SpanUtils.endSpan(spanInfo.span, {
966989
code: SpanStatusCode.OK,
967990
});
968-
return Array.isArray(documents) ? documents : [documents];
991+
return documents;
969992
} catch (error: any) {
970993
SpanUtils.endSpan(spanInfo.span, {
971994
code: SpanStatusCode.ERROR,
@@ -1401,7 +1424,7 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
14011424
return resultPromise
14021425
.then((result: any) => {
14031426
try {
1404-
addOutputAttributesToSpan(spanInfo, result);
1427+
addOutputAttributesToSpan(spanInfo, wrapDirectOutput(result));
14051428
SpanUtils.endSpan(spanInfo.span, {
14061429
code: SpanStatusCode.OK,
14071430
});
@@ -1493,7 +1516,9 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
14931516
throw new Error(errorMsg);
14941517
}
14951518

1496-
const result = reconstructBsonValue(mockData.result, this.moduleExports);
1519+
const result = unwrapDirectOutput(
1520+
reconstructBsonValue(mockData.result, this.moduleExports),
1521+
);
14971522

14981523
SpanUtils.endSpan(spanInfo.span, { code: SpanStatusCode.OK });
14991524
return result;
@@ -1787,12 +1812,13 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
17871812
throw new Error(errorMsg);
17881813
}
17891814

1790-
const documents = reconstructBsonValue(mockData.result, self.moduleExports);
1815+
const reconstructed = reconstructBsonValue(mockData.result, self.moduleExports);
1816+
const documents = unwrapCursorOutput(reconstructed);
17911817

17921818
SpanUtils.endSpan(spanInfo.span, {
17931819
code: SpanStatusCode.OK,
17941820
});
1795-
return Array.isArray(documents) ? documents : [documents];
1821+
return documents;
17961822
} catch (error: any) {
17971823
SpanUtils.endSpan(spanInfo.span, {
17981824
code: SpanStatusCode.ERROR,
@@ -2252,6 +2278,87 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
22522278
// Bulk Operations (Ordered/Unordered)
22532279
// ---------------------------------------------------------------------------
22542280

2281+
/**
2282+
* Patch Collection.prototype.initializeOrderedBulkOp and
2283+
* Collection.prototype.initializeUnorderedBulkOp.
2284+
*
2285+
* In replay mode, BulkOperationBase's constructor calls getTopology(collection)
2286+
* which throws if no topology is connected. We inject a FakeTopology onto the
2287+
* collection (and its client) BEFORE calling the original, so the constructor
2288+
* finds a valid topology object and proceeds with default size limits.
2289+
*/
2290+
private _patchBulkOpInitMethods(actualExports: any): void {
2291+
const Collection = actualExports.Collection;
2292+
if (!Collection || !Collection.prototype) {
2293+
logger.warn(
2294+
`[${this.INSTRUMENTATION_NAME}] Collection not found, skipping bulk op init patching`,
2295+
);
2296+
return;
2297+
}
2298+
2299+
const self = this;
2300+
2301+
// Patch initializeOrderedBulkOp
2302+
if (typeof Collection.prototype.initializeOrderedBulkOp === "function") {
2303+
this._wrap(
2304+
Collection.prototype,
2305+
"initializeOrderedBulkOp",
2306+
(original: Function) => {
2307+
return function (this: any, ...args: any[]) {
2308+
if (self.mode === TuskDriftMode.REPLAY) {
2309+
self._injectFakeTopology(this);
2310+
}
2311+
return original.apply(this, args);
2312+
};
2313+
},
2314+
);
2315+
logger.debug(
2316+
`[${this.INSTRUMENTATION_NAME}] Wrapped Collection.prototype.initializeOrderedBulkOp`,
2317+
);
2318+
}
2319+
2320+
// Patch initializeUnorderedBulkOp
2321+
if (typeof Collection.prototype.initializeUnorderedBulkOp === "function") {
2322+
this._wrap(
2323+
Collection.prototype,
2324+
"initializeUnorderedBulkOp",
2325+
(original: Function) => {
2326+
return function (this: any, ...args: any[]) {
2327+
if (self.mode === TuskDriftMode.REPLAY) {
2328+
self._injectFakeTopology(this);
2329+
}
2330+
return original.apply(this, args);
2331+
};
2332+
},
2333+
);
2334+
logger.debug(
2335+
`[${this.INSTRUMENTATION_NAME}] Wrapped Collection.prototype.initializeUnorderedBulkOp`,
2336+
);
2337+
}
2338+
}
2339+
2340+
/**
2341+
* Inject a FakeTopology onto a collection and its client for replay mode.
2342+
*
2343+
* getTopology() in the MongoDB driver checks:
2344+
* 1. provider.topology (direct property on collection)
2345+
* 2. provider.client.topology (via the MongoClient)
2346+
* We set both to ensure the topology lookup succeeds.
2347+
*/
2348+
private _injectFakeTopology(collection: any): void {
2349+
const fakeTopology = new TdFakeTopology();
2350+
2351+
// Set on the client (satisfies getTopology's client.topology check)
2352+
if (collection.client && !collection.client.topology) {
2353+
collection.client.topology = fakeTopology;
2354+
}
2355+
2356+
// Set on collection directly as fallback
2357+
if (!collection.topology) {
2358+
collection.topology = fakeTopology;
2359+
}
2360+
}
2361+
22552362
/**
22562363
* Patch OrderedBulkOperation.prototype.execute from mongodb/lib/bulk/ordered.js.
22572364
*/
@@ -2560,7 +2667,7 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
25602667
return resultPromise
25612668
.then((result: any) => {
25622669
try {
2563-
addOutputAttributesToSpan(spanInfo, result);
2670+
addOutputAttributesToSpan(spanInfo, wrapDirectOutput(result));
25642671
SpanUtils.endSpan(spanInfo.span, {
25652672
code: SpanStatusCode.OK,
25662673
});
@@ -2660,7 +2767,9 @@ export class MongodbInstrumentation extends TdInstrumentationBase {
26602767
throw new Error(errorMsg);
26612768
}
26622769

2663-
const result = reconstructBsonValue(mockData.result, this.moduleExports);
2770+
const result = unwrapDirectOutput(
2771+
reconstructBsonValue(mockData.result, this.moduleExports),
2772+
);
26642773

26652774
SpanUtils.endSpan(spanInfo.span, { code: SpanStatusCode.OK });
26662775
return result;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
version: 1 # version of the config file format
2+
3+
service:
4+
id: "cjs-mongodb-e2e-test-id"
5+
name: "cjs-mongodb-e2e-test"
6+
port: 3000
7+
start:
8+
command: "npm run dev"
9+
readiness_check:
10+
command: "curl http://localhost:3000/health"
11+
timeout: 45s
12+
interval: 5s
13+
14+
tusk_api:
15+
url: "http://localhost:3000"
16+
17+
test_execution:
18+
concurrent_limit: 10
19+
batch_size: 10
20+
timeout: 30s
21+
22+
comparison:
23+
ignore_fields:
24+
- created_at
25+
26+
recording:
27+
sampling_rate: 1.0 # 100%
28+
export_spans: false
29+
30+
replay:
31+
enable_telemetry: false
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
FROM node:18
2+
3+
WORKDIR /app
4+
5+
# Copy package files from e2e test directory
6+
COPY src/instrumentation/libraries/mongodb/e2e-tests/cjs-mongodb/package*.json ./
7+
COPY src/instrumentation/libraries/mongodb/e2e-tests/cjs-mongodb/tsconfig.json ./
8+
9+
# Add cache-busting argument to force fresh CLI download
10+
ARG CACHEBUST=1
11+
ARG TUSK_CLI_VERSION=latest
12+
13+
# Install Tusk Drift CLI
14+
RUN if [ "$TUSK_CLI_VERSION" = "latest" ]; then \
15+
curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh; \
16+
else \
17+
curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh -s -- ${TUSK_CLI_VERSION}; \
18+
fi
19+
20+
# Expose the server port
21+
EXPOSE 3000
22+
23+
COPY src/instrumentation/libraries/mongodb/e2e-tests/cjs-mongodb/entrypoint.sh ./
24+
RUN chmod +x entrypoint.sh
25+
RUN mkdir -p /app/.tusk/traces /app/.tusk/logs
26+
COPY src/instrumentation/libraries/e2e-common/base-entrypoint.sh /app/base-entrypoint.sh
27+
COPY src/instrumentation/libraries/e2e-common/test-utils.mjs /app/test-utils.mjs
28+
RUN chmod +x /app/base-entrypoint.sh
29+
ENTRYPOINT ["./entrypoint.sh"]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
services:
2+
mongo:
3+
image: mongo:7
4+
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
5+
healthcheck:
6+
test: >
7+
mongosh --quiet --eval "
8+
try {
9+
rs.status().ok && 1
10+
} catch(e) {
11+
rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongo:27017'}]});
12+
sleep(2000);
13+
rs.status().ok && 1
14+
}
15+
" || exit 1
16+
interval: 5s
17+
timeout: 10s
18+
retries: 10
19+
20+
app:
21+
build:
22+
context: ../../../../../..
23+
dockerfile: src/instrumentation/libraries/mongodb/e2e-tests/cjs-mongodb/Dockerfile
24+
args:
25+
- CACHEBUST=${CACHEBUST:-1}
26+
- TUSK_CLI_VERSION=${TUSK_CLI_VERSION:-latest}
27+
environment:
28+
- BENCHMARKS=${BENCHMARKS:-}
29+
- BENCHMARK_DURATION=${BENCHMARK_DURATION:-5}
30+
- BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3}
31+
- PORT=3000
32+
- MONGO_HOST=mongo
33+
- MONGO_PORT=27017
34+
- MONGO_DB=testdb
35+
- TUSK_ANALYTICS_DISABLED=1
36+
volumes:
37+
# Mount SDK source for hot reload (this is what package.json expects)
38+
- ../../../../../..:/sdk:ro
39+
# Mount .tusk config to persist configuration
40+
- ./.tusk/config.yaml:/app/.tusk/config.yaml:ro
41+
# Persist traces and logs on host
42+
- ./.tusk/traces:/app/.tusk/traces
43+
- ./.tusk/logs:/app/.tusk/logs
44+
# Mount app source for development
45+
- ./src:/app/src
46+
working_dir: /app
47+
depends_on:
48+
mongo:
49+
condition: service_healthy
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
SERVER_WAIT_TIME=8
3+
source /app/base-entrypoint.sh

0 commit comments

Comments
 (0)