@reproapp/node-sdk 0.0.38 → 0.0.41

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.
@@ -0,0 +1,302 @@
1
+ process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '50';
2
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '25';
3
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
4
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
5
+
6
+ const assert = require('assert');
7
+ const { flushIngestQueue } = require('../dist/ingest/client');
8
+ const { patchPostgreSql, __reproTestHooks } = require('../dist');
9
+
10
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+
12
+ async function waitFor(predicate, timeoutMs = 2000) {
13
+ const deadline = Date.now() + timeoutMs;
14
+ while (Date.now() < deadline) {
15
+ if (predicate()) return;
16
+ await sleep(20);
17
+ }
18
+ assert(predicate(), 'timed out waiting for expected PostgreSQL evidence');
19
+ }
20
+
21
+ function flattenEvents(posts) {
22
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
23
+ }
24
+
25
+ function dbChanges(posts) {
26
+ return flattenEvents(posts)
27
+ .filter((event) => event.event_type === 'db_change')
28
+ .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
29
+ }
30
+
31
+ function dbValues(posts) {
32
+ return flattenEvents(posts)
33
+ .filter((event) => event.event_type === 'db_change')
34
+ .flatMap((event) => Array.isArray(event.payload?.dbValues) ? event.payload.dbValues : []);
35
+ }
36
+
37
+ async function drainSdk() {
38
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
39
+ await flushIngestQueue();
40
+ }
41
+
42
+ async function capture(fn) {
43
+ const posts = [];
44
+ const originalFetch = global.fetch;
45
+ global.fetch = async (_url, init) => {
46
+ posts.push(JSON.parse(String(init?.body ?? '{}')));
47
+ return {
48
+ ok: true,
49
+ status: 202,
50
+ json: async () => ({ ok: true }),
51
+ text: async () => '{"ok":true}',
52
+ };
53
+ };
54
+ try {
55
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
56
+ await fn(posts);
57
+ await waitFor(() => dbChanges(posts).length > 0);
58
+ await drainSdk();
59
+ return posts;
60
+ } finally {
61
+ await drainSdk().catch(() => {});
62
+ global.fetch = originalFetch;
63
+ }
64
+ }
65
+
66
+ function config() {
67
+ return {
68
+ tenantId: 'TENANT_postgresql_test',
69
+ appId: 'APP_postgresql_test',
70
+ appSecret: 'secret',
71
+ captureHeaders: false,
72
+ privacy: { environment: 'dev', captureRaw: true },
73
+ ingestBase: 'http://127.0.0.1:65535',
74
+ };
75
+ }
76
+
77
+ class FakePgClient {
78
+ query(input, values, callback) {
79
+ const cb = typeof values === 'function' ? values : callback;
80
+ const text = typeof input === 'string' ? input : input.text;
81
+ const params = Array.isArray(values) ? values : input.values || [];
82
+ const execute = () => {
83
+ if (text.includes('broken')) {
84
+ const error = Object.assign(new Error('relation does not exist'), {
85
+ code: '42P01',
86
+ severity: 'ERROR',
87
+ table: 'missing_table',
88
+ });
89
+ if (cb) return void setImmediate(() => cb(error));
90
+ return Promise.reject(error);
91
+ }
92
+ const result = {
93
+ command: text.trim().split(/\s+/)[0].toUpperCase(),
94
+ rowCount: 1,
95
+ fields: [{ name: 'email', dataTypeID: 25 }],
96
+ rows: [{ email: params[0], nested: { complete: true } }],
97
+ };
98
+ if (cb) return void setImmediate(() => cb(null, result));
99
+ return Promise.resolve(result);
100
+ };
101
+ return execute();
102
+ }
103
+ }
104
+
105
+ class FakePostgresQuery extends Promise {
106
+ constructor(strings, args, result) {
107
+ let resolve;
108
+ let reject;
109
+ super((res, rej) => {
110
+ resolve = res;
111
+ reject = rej;
112
+ });
113
+ this.tagged = Array.isArray(strings?.raw);
114
+ this.strings = strings;
115
+ this.args = args;
116
+ this.result = result;
117
+ this.resolveQuery = resolve;
118
+ this.rejectQuery = reject;
119
+ this.executed = false;
120
+ }
121
+
122
+ static get [Symbol.species]() {
123
+ return Promise;
124
+ }
125
+
126
+ handle() {
127
+ if (this.executed) return;
128
+ this.executed = true;
129
+ setImmediate(() => this.resolveQuery(this.result));
130
+ }
131
+
132
+ then(...args) {
133
+ this.handle();
134
+ return super.then(...args);
135
+ }
136
+
137
+ catch(...args) {
138
+ this.handle();
139
+ return super.catch(...args);
140
+ }
141
+
142
+ finally(...args) {
143
+ this.handle();
144
+ return super.finally(...args);
145
+ }
146
+ }
147
+
148
+ function makeFakePostgresModule() {
149
+ const result = [{ id: 7, title: 'postgres.js row', details: { retained: true } }];
150
+ result.command = 'SELECT';
151
+ result.count = 1;
152
+ result.columns = [{ name: 'title', type: 25, table: 42, number: 2 }];
153
+
154
+ function postgres() {
155
+ const sql = (strings, ...args) => new FakePostgresQuery(strings, args, result);
156
+ sql.unsafe = (text, args = []) => new FakePostgresQuery([text], args, result);
157
+ sql.end = async () => {};
158
+ return sql;
159
+ }
160
+ postgres.__repro_postgres_factory = true;
161
+ return postgres;
162
+ }
163
+
164
+ async function testPgPromiseCallbackAndError() {
165
+ const cfg = config();
166
+ patchPostgreSql(cfg);
167
+ __reproTestHooks.patchPgModuleForTest({ Client: FakePgClient });
168
+ const posts = await capture(async (capturedPosts) => {
169
+ await __reproTestHooks.runWithRequestContextForTest(
170
+ { sid: 'S_pg', aid: 'A_pg', rid: 'R_pg' },
171
+ async () => {
172
+ const client = new FakePgClient();
173
+ const result = await client.query({
174
+ text: 'SELECT email FROM app_users WHERE email = $1',
175
+ values: ['pg-user@getrepro.local'],
176
+ name: 'find-app-user',
177
+ });
178
+ assert.strictEqual(result.rows[0].nested.complete, true);
179
+
180
+ await new Promise((resolve, reject) => {
181
+ client.query(
182
+ 'INSERT INTO app_users(email) VALUES($1) RETURNING email',
183
+ ['callback@getrepro.local'],
184
+ (error, callbackResult) => {
185
+ if (error) reject(error);
186
+ else {
187
+ assert.strictEqual(callbackResult.rows[0].email, 'callback@getrepro.local');
188
+ resolve();
189
+ }
190
+ },
191
+ );
192
+ });
193
+
194
+ await assert.rejects(
195
+ client.query('SELECT * FROM broken_table', []),
196
+ (error) => error.code === '42P01',
197
+ );
198
+ },
199
+ );
200
+ await waitFor(() => dbChanges(capturedPosts).length === 3);
201
+ });
202
+
203
+ const changes = dbChanges(posts);
204
+ assert.strictEqual(changes.length, 3);
205
+ assert.deepStrictEqual(changes.map((change) => change.op), ['select', 'insert', 'select']);
206
+ assert.strictEqual(changes[0].type, 'postgresql');
207
+ assert.strictEqual(changes[0].collection, 'app_users');
208
+ assert.strictEqual(changes[0].query.library, 'pg');
209
+ assert.deepStrictEqual(changes[0].query.parameters, ['pg-user@getrepro.local']);
210
+ assert.strictEqual(changes[1].collection, 'app_users');
211
+ assert.strictEqual(changes[2].error.code, '42P01');
212
+ assert(dbValues(posts).some((entry) => (
213
+ entry.target === 'db.resultMeta' &&
214
+ entry.value?.rows?.[0]?.nested?.complete === true
215
+ )), JSON.stringify(dbValues(posts)));
216
+ }
217
+
218
+ async function testPostgresJsLazyExecution() {
219
+ const cfg = config();
220
+ patchPostgreSql(cfg);
221
+ const postgres = makeFakePostgresModule();
222
+ __reproTestHooks.patchPostgresJsModuleForTest(postgres);
223
+
224
+ const posts = await capture(async (capturedPosts) => {
225
+ await __reproTestHooks.runWithRequestContextForTest(
226
+ { sid: 'S_postgres_js', aid: 'A_postgres_js', rid: 'R_postgres_js' },
227
+ async () => {
228
+ const sql = postgres();
229
+ const title = 'lazy execution';
230
+ const query = sql`SELECT id, title, details FROM work_items WHERE title = ${title}`;
231
+ await sleep(20);
232
+ assert.strictEqual(dbChanges(capturedPosts).length, 0, 'unexecuted lazy query must not emit evidence');
233
+ const rows = await query;
234
+ assert.strictEqual(rows[0].details.retained, true);
235
+ },
236
+ );
237
+ });
238
+
239
+ const [change] = dbChanges(posts);
240
+ assert.strictEqual(change.type, 'postgresql');
241
+ assert.strictEqual(change.collection, 'work_items');
242
+ assert.strictEqual(change.query.library, 'postgres');
243
+ assert.strictEqual(change.query.driver, 'postgres');
244
+ assert.strictEqual(change.query.sql, 'SELECT id, title, details FROM work_items WHERE title = $1');
245
+ assert.deepStrictEqual(change.query.parameters, ['lazy execution']);
246
+ assert(dbValues(posts).some((entry) => (
247
+ entry.target === 'db.resultMeta' &&
248
+ entry.value?.rows?.[0]?.details?.retained === true
249
+ )), JSON.stringify(dbValues(posts)));
250
+ }
251
+
252
+ async function testTypeOrmDelegatesToPgWithoutDuplicateEvidence() {
253
+ const cfg = config();
254
+ patchPostgreSql(cfg);
255
+ __reproTestHooks.patchPgModuleForTest({ Client: FakePgClient });
256
+
257
+ class FakeDataSource {
258
+ constructor(client) {
259
+ this.client = client;
260
+ }
261
+
262
+ createQueryRunner() {
263
+ return {
264
+ query: (sql, parameters) => this.client.query(sql, parameters),
265
+ };
266
+ }
267
+ }
268
+
269
+ __reproTestHooks.patchTypeOrmModuleForTest({ DataSource: FakeDataSource });
270
+ const posts = await capture(async () => {
271
+ await __reproTestHooks.runWithRequestContextForTest(
272
+ { sid: 'S_typeorm', aid: 'A_typeorm', rid: 'R_typeorm' },
273
+ async () => {
274
+ const dataSource = new FakeDataSource(new FakePgClient());
275
+ const result = await dataSource
276
+ .createQueryRunner()
277
+ .query('UPDATE inventory SET quantity = $1 WHERE id = $2 RETURNING *', [19, 7]);
278
+ assert.strictEqual(result.rowCount, 1);
279
+ },
280
+ );
281
+ });
282
+
283
+ const changes = dbChanges(posts);
284
+ assert.strictEqual(changes.length, 1, JSON.stringify(changes));
285
+ assert.strictEqual(changes[0].op, 'update');
286
+ assert.strictEqual(changes[0].collection, 'inventory');
287
+ assert.strictEqual(changes[0].query.library, 'typeorm');
288
+ assert.strictEqual(changes[0].query.driver, 'pg');
289
+ assert.deepStrictEqual(changes[0].query.parameters, [19, 7]);
290
+ }
291
+
292
+ async function main() {
293
+ await testPgPromiseCallbackAndError();
294
+ await testPostgresJsLazyExecution();
295
+ await testTypeOrmDelegatesToPgWithoutDuplicateEvidence();
296
+ console.log('postgresql integration tests OK');
297
+ }
298
+
299
+ main().catch((error) => {
300
+ console.error(error);
301
+ process.exit(1);
302
+ });
@@ -0,0 +1,261 @@
1
+ process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '100';
2
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '25';
3
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
4
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
5
+
6
+ const assert = require('assert');
7
+ require('reflect-metadata');
8
+ const { GenericContainer, Wait } = require('testcontainers');
9
+ const { flushIngestQueue } = require('../dist/ingest/client');
10
+ const { patchPostgreSql, __reproTestHooks } = require('../dist');
11
+
12
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+
14
+ async function waitFor(predicate, timeoutMs = 5000) {
15
+ const deadline = Date.now() + timeoutMs;
16
+ while (Date.now() < deadline) {
17
+ if (predicate()) return;
18
+ await sleep(25);
19
+ }
20
+ assert(predicate(), 'timed out waiting for PostgreSQL evidence materialization');
21
+ }
22
+
23
+ function flattenEvents(posts) {
24
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
25
+ }
26
+
27
+ function dbChanges(posts) {
28
+ return flattenEvents(posts)
29
+ .filter((event) => event.event_type === 'db_change')
30
+ .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
31
+ }
32
+
33
+ function dbValues(posts) {
34
+ return flattenEvents(posts)
35
+ .filter((event) => event.event_type === 'db_change')
36
+ .flatMap((event) => Array.isArray(event.payload?.dbValues) ? event.payload.dbValues : []);
37
+ }
38
+
39
+ async function startPostgres() {
40
+ if (process.env.REPRO_POSTGRES_TEST_URL) {
41
+ return { url: process.env.REPRO_POSTGRES_TEST_URL, stop: async () => {} };
42
+ }
43
+ const container = await new GenericContainer('postgres:16-alpine')
44
+ .withEnvironment({
45
+ POSTGRES_USER: 'repro',
46
+ POSTGRES_PASSWORD: 'repro',
47
+ POSTGRES_DB: 'repro_sdk_test',
48
+ })
49
+ .withExposedPorts(5432)
50
+ .withTmpFs({ '/var/lib/postgresql/data': 'rw,noexec,nosuid,size=1g' })
51
+ .withWaitStrategy(Wait.forLogMessage(/database system is ready to accept connections/))
52
+ .start();
53
+ return {
54
+ url: `postgresql://repro:repro@${container.getHost()}:${container.getMappedPort(5432)}/repro_sdk_test`,
55
+ stop: () => container.stop(),
56
+ };
57
+ }
58
+
59
+ async function main() {
60
+ const posts = [];
61
+ const originalFetch = global.fetch;
62
+ global.fetch = async (_url, init) => {
63
+ posts.push(JSON.parse(String(init?.body ?? '{}')));
64
+ return {
65
+ ok: true,
66
+ status: 202,
67
+ json: async () => ({ ok: true }),
68
+ text: async () => '{"ok":true}',
69
+ };
70
+ };
71
+
72
+ let postgresContainer;
73
+ let pool;
74
+ let postgresSql;
75
+ let standaloneDataSource;
76
+ let nestContext;
77
+ try {
78
+ postgresContainer = await startPostgres();
79
+ const cfg = {
80
+ tenantId: 'TENANT_postgresql_container',
81
+ appId: 'APP_postgresql_container',
82
+ appSecret: 'secret',
83
+ captureHeaders: false,
84
+ privacy: { environment: 'dev', captureRaw: true },
85
+ ingestBase: 'http://127.0.0.1:65535',
86
+ };
87
+ patchPostgreSql(cfg);
88
+
89
+ const { Pool } = require('pg');
90
+ const postgres = require('postgres');
91
+ const { DataSource, EntitySchema } = require('typeorm');
92
+ const { Injectable, Module } = require('@nestjs/common');
93
+ const { NestFactory } = require('@nestjs/core');
94
+ const { InjectRepository, TypeOrmModule } = require('@nestjs/typeorm');
95
+
96
+ pool = new Pool({ connectionString: postgresContainer.url });
97
+ postgresSql = postgres(postgresContainer.url, { max: 2 });
98
+ await pool.query(`
99
+ CREATE TABLE IF NOT EXISTS pg_accounts (
100
+ id SERIAL PRIMARY KEY,
101
+ email TEXT NOT NULL,
102
+ profile JSONB NOT NULL
103
+ )
104
+ `);
105
+ await pool.query(`
106
+ CREATE TABLE IF NOT EXISTS postgres_jobs (
107
+ id SERIAL PRIMARY KEY,
108
+ title TEXT NOT NULL,
109
+ metadata JSONB NOT NULL
110
+ )
111
+ `);
112
+
113
+ const StandaloneRecord = new EntitySchema({
114
+ name: 'StandaloneRecord',
115
+ tableName: 'typeorm_standalone_records',
116
+ columns: {
117
+ id: { type: Number, primary: true, generated: true },
118
+ label: { type: String },
119
+ details: { type: 'jsonb' },
120
+ },
121
+ });
122
+ standaloneDataSource = new DataSource({
123
+ type: 'postgres',
124
+ url: postgresContainer.url,
125
+ synchronize: true,
126
+ entities: [StandaloneRecord],
127
+ });
128
+ await standaloneDataSource.initialize();
129
+
130
+ const NestRecord = new EntitySchema({
131
+ name: 'NestRecord',
132
+ tableName: 'typeorm_nest_records',
133
+ columns: {
134
+ id: { type: Number, primary: true, generated: true },
135
+ actorId: { type: String },
136
+ payload: { type: 'jsonb' },
137
+ },
138
+ });
139
+
140
+ class NestRecordService {
141
+ constructor(repository) {
142
+ this.repository = repository;
143
+ }
144
+ async create(actorId) {
145
+ return this.repository.save(this.repository.create({
146
+ actorId,
147
+ payload: { source: 'nestjs-typeorm', complete: true },
148
+ }));
149
+ }
150
+ async find(actorId) {
151
+ return this.repository.find({ where: { actorId } });
152
+ }
153
+ }
154
+ Injectable()(NestRecordService);
155
+ InjectRepository(NestRecord)(NestRecordService, undefined, 0);
156
+
157
+ class NestPgModule {}
158
+ Module({
159
+ imports: [
160
+ TypeOrmModule.forRoot({
161
+ type: 'postgres',
162
+ url: postgresContainer.url,
163
+ synchronize: true,
164
+ entities: [NestRecord],
165
+ }),
166
+ TypeOrmModule.forFeature([NestRecord]),
167
+ ],
168
+ providers: [NestRecordService],
169
+ })(NestPgModule);
170
+ nestContext = await NestFactory.createApplicationContext(NestPgModule, { logger: false });
171
+ const nestService = nestContext.get(NestRecordService);
172
+
173
+ await __reproTestHooks.runWithRequestContextForTest(
174
+ { sid: 'S_postgresql_all', aid: 'A_postgresql_all', rid: 'R_postgresql_all' },
175
+ async () => {
176
+ const pgInsert = await pool.query(
177
+ 'INSERT INTO pg_accounts(email, profile) VALUES($1, $2) RETURNING *',
178
+ ['container@getrepro.local', { driver: 'pg', complete: true }],
179
+ );
180
+ assert.strictEqual(pgInsert.rows[0].profile.complete, true);
181
+
182
+ const pgSelected = await pool.query(
183
+ 'SELECT * FROM pg_accounts WHERE email = $1',
184
+ ['container@getrepro.local'],
185
+ );
186
+ assert.strictEqual(pgSelected.rowCount, 1);
187
+
188
+ const [postgresJob] = await postgresSql`
189
+ INSERT INTO postgres_jobs(title, metadata)
190
+ VALUES (${'container job'}, ${postgresSql.json({ driver: 'postgres', complete: true })})
191
+ RETURNING *
192
+ `;
193
+ assert.strictEqual(postgresJob.metadata.complete, true);
194
+
195
+ const standaloneRepository = standaloneDataSource.getRepository(StandaloneRecord);
196
+ await standaloneRepository.save(standaloneRepository.create({
197
+ label: 'standalone typeorm',
198
+ details: { source: 'typeorm', complete: true },
199
+ }));
200
+ const standaloneRows = await standaloneRepository.findBy({ label: 'standalone typeorm' });
201
+ assert.strictEqual(standaloneRows[0].details.complete, true);
202
+
203
+ await nestService.create('actor-nest-container');
204
+ const nestRows = await nestService.find('actor-nest-container');
205
+ assert.strictEqual(nestRows[0].payload.complete, true);
206
+ },
207
+ );
208
+
209
+ await waitFor(() => dbChanges(posts).length >= 12);
210
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
211
+ await flushIngestQueue();
212
+
213
+ const changes = dbChanges(posts);
214
+ const values = dbValues(posts);
215
+ const pgAccountInserts = changes.filter((change) => (
216
+ change.collection === 'pg_accounts' && change.op === 'insert'
217
+ ));
218
+ const postgresJobInserts = changes.filter((change) => (
219
+ change.collection === 'postgres_jobs' && change.op === 'insert'
220
+ ));
221
+ const standaloneInserts = changes.filter((change) => (
222
+ change.collection === 'typeorm_standalone_records' && change.op === 'insert'
223
+ ));
224
+ const nestInserts = changes.filter((change) => (
225
+ change.collection === 'typeorm_nest_records' && change.op === 'insert'
226
+ ));
227
+
228
+ assert.strictEqual(pgAccountInserts.length, 1, JSON.stringify(changes));
229
+ assert.strictEqual(pgAccountInserts[0].query.library, 'pg');
230
+ assert.strictEqual(postgresJobInserts.length, 1, JSON.stringify(postgresJobInserts));
231
+ assert.strictEqual(postgresJobInserts[0].query.library, 'postgres');
232
+ assert.strictEqual(standaloneInserts.length, 1, JSON.stringify(standaloneInserts));
233
+ assert.strictEqual(standaloneInserts[0].query.library, 'typeorm');
234
+ assert.strictEqual(nestInserts.length, 1, JSON.stringify(changes));
235
+ assert.strictEqual(nestInserts[0].query.library, 'typeorm');
236
+ assert(values.some((entry) => (
237
+ entry.target === 'db.resultMeta' &&
238
+ JSON.stringify(entry.value).includes('container@getrepro.local')
239
+ )), 'expected complete pg result rows in a DB value sidecar');
240
+ assert(values.some((entry) => (
241
+ entry.target === 'db.resultMeta' &&
242
+ JSON.stringify(entry.value).includes('actor-nest-container')
243
+ )), 'expected complete Nest TypeORM result rows in a DB value sidecar');
244
+
245
+ console.log(`postgresql testcontainers integrations OK (${changes.length} captured statements)`);
246
+ } finally {
247
+ await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
248
+ await flushIngestQueue().catch(() => {});
249
+ global.fetch = originalFetch;
250
+ if (nestContext) await nestContext.close().catch(() => {});
251
+ if (standaloneDataSource?.isInitialized) await standaloneDataSource.destroy().catch(() => {});
252
+ if (postgresSql) await postgresSql.end({ timeout: 1 }).catch(() => {});
253
+ if (pool) await pool.end().catch(() => {});
254
+ if (postgresContainer) await postgresContainer.stop().catch(() => {});
255
+ }
256
+ }
257
+
258
+ main().catch((error) => {
259
+ console.error(error);
260
+ process.exit(1);
261
+ });
@@ -124,11 +124,17 @@ async function main() {
124
124
  .some((event) => event?.event_type === 'request_capture_manifest'));
125
125
 
126
126
  const eventTypes = flattenEvents(capturedBodies).map((event) => event?.event_type);
127
+ const startIndex = eventTypes.indexOf('request_capture_start');
127
128
  const manifestIndex = eventTypes.indexOf('request_capture_manifest');
128
129
  const traceIndex = eventTypes.indexOf('trace_batch');
129
130
 
131
+ assert(startIndex >= 0, JSON.stringify(eventTypes));
130
132
  assert(manifestIndex >= 0, JSON.stringify(eventTypes));
131
133
  assert(traceIndex >= 0, JSON.stringify(eventTypes));
134
+ assert(
135
+ startIndex < manifestIndex,
136
+ `request_capture_start must be sent before request_capture_manifest; got ${JSON.stringify(eventTypes)}`,
137
+ );
132
138
  assert(
133
139
  manifestIndex < traceIndex,
134
140
  `request_capture_manifest must be sent before trace_batch; got ${JSON.stringify(eventTypes)}`,