@syncular/server-hono 0.0.2-138 → 0.0.2-140

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.
package/dist/routes.js CHANGED
@@ -25,6 +25,179 @@ const realtimeUnsubscribeMap = new WeakMap();
25
25
  const snapshotChunkParamsSchema = z.object({
26
26
  chunkId: z.string().min(1),
27
27
  });
28
+ const MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES = 128 * 1024;
29
+ function createOpaqueId(prefix) {
30
+ const randomPart = typeof crypto !== 'undefined' && 'randomUUID' in crypto
31
+ ? crypto.randomUUID()
32
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
33
+ return `${prefix}-${randomPart}`;
34
+ }
35
+ function readRequestId(c) {
36
+ const headerRequestId = c.req.header('x-request-id')?.trim();
37
+ if (headerRequestId)
38
+ return headerRequestId;
39
+ return createOpaqueId('req');
40
+ }
41
+ function parseW3cTraceparent(traceparent) {
42
+ if (!traceparent)
43
+ return null;
44
+ const parsed = traceparent.trim();
45
+ const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i.exec(parsed);
46
+ if (!match)
47
+ return null;
48
+ const traceId = match[1]?.toLowerCase() ?? null;
49
+ const spanId = match[2]?.toLowerCase() ?? null;
50
+ if (!traceId || !spanId)
51
+ return null;
52
+ return { traceId, spanId };
53
+ }
54
+ function parseSentryTraceHeader(sentryTrace) {
55
+ if (!sentryTrace)
56
+ return null;
57
+ const parsed = sentryTrace.trim();
58
+ const match = /^([0-9a-f]{32})-([0-9a-f]{16})(?:-[01])?$/i.exec(parsed);
59
+ if (!match)
60
+ return null;
61
+ const traceId = match[1]?.toLowerCase() ?? null;
62
+ const spanId = match[2]?.toLowerCase() ?? null;
63
+ if (!traceId || !spanId)
64
+ return null;
65
+ return { traceId, spanId };
66
+ }
67
+ function readTraceContext(c) {
68
+ const traceparent = parseW3cTraceparent(c.req.header('traceparent'));
69
+ if (traceparent)
70
+ return traceparent;
71
+ const sentryTrace = parseSentryTraceHeader(c.req.header('sentry-trace'));
72
+ if (sentryTrace)
73
+ return sentryTrace;
74
+ return { traceId: null, spanId: null };
75
+ }
76
+ function readStringField(data, key) {
77
+ const value = data[key];
78
+ if (typeof value !== 'string')
79
+ return null;
80
+ const trimmed = value.trim();
81
+ return trimmed.length > 0 ? trimmed : null;
82
+ }
83
+ function readTraceContextFromMessage(msg) {
84
+ const directTraceId = readStringField(msg, 'traceId') ?? readStringField(msg, 'trace_id');
85
+ const directSpanId = readStringField(msg, 'spanId') ?? readStringField(msg, 'span_id');
86
+ if (directTraceId || directSpanId) {
87
+ return { traceId: directTraceId, spanId: directSpanId };
88
+ }
89
+ const traceparent = readStringField(msg, 'traceparent') ?? readStringField(msg, 'traceParent');
90
+ const parsedTraceparent = parseW3cTraceparent(traceparent);
91
+ if (parsedTraceparent)
92
+ return parsedTraceparent;
93
+ const sentryTrace = readStringField(msg, 'sentry-trace') ??
94
+ readStringField(msg, 'sentryTrace') ??
95
+ readStringField(msg, 'sentry_trace');
96
+ const parsedSentryTrace = parseSentryTraceHeader(sentryTrace);
97
+ if (parsedSentryTrace)
98
+ return parsedSentryTrace;
99
+ return { traceId: null, spanId: null };
100
+ }
101
+ function normalizeResponseStatus(statusCode, outcome) {
102
+ if (statusCode >= 500)
103
+ return 'server_error';
104
+ if (statusCode >= 400)
105
+ return 'client_error';
106
+ if (statusCode >= 300)
107
+ return 'redirect';
108
+ if (statusCode >= 200) {
109
+ if (outcome === 'error' || outcome === 'rejected')
110
+ return 'failure';
111
+ return 'success';
112
+ }
113
+ return 'unknown';
114
+ }
115
+ function firstPushErrorCode(results) {
116
+ if (!Array.isArray(results))
117
+ return null;
118
+ for (const result of results) {
119
+ if (!result || typeof result !== 'object')
120
+ continue;
121
+ const status = Reflect.get(result, 'status');
122
+ if (status !== 'error')
123
+ continue;
124
+ const code = Reflect.get(result, 'code');
125
+ if (typeof code === 'string' && code.length > 0) {
126
+ return code;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ function summarizeScopeValues(scopes) {
132
+ const summary = {};
133
+ for (const [key, value] of Object.entries(scopes)) {
134
+ if (typeof value === 'string') {
135
+ summary[key] = value;
136
+ continue;
137
+ }
138
+ if (Array.isArray(value)) {
139
+ const normalized = value
140
+ .filter((entry) => typeof entry === 'string')
141
+ .slice(0, 20);
142
+ summary[key] = normalized;
143
+ }
144
+ }
145
+ return Object.keys(summary).length > 0 ? summary : null;
146
+ }
147
+ function summarizePullResponse(response) {
148
+ return {
149
+ subscriptions: response.subscriptions.map((subscription) => {
150
+ const changeCount = subscription.commits.reduce((totalChanges, commit) => totalChanges + commit.changes.length, 0);
151
+ const snapshotCount = subscription.snapshots?.length ?? 0;
152
+ const snapshotRowCount = subscription.snapshots?.reduce((totalRows, snapshot) => totalRows + snapshot.rows.length, 0) ?? 0;
153
+ return {
154
+ id: subscription.id,
155
+ status: subscription.status,
156
+ bootstrap: subscription.bootstrap,
157
+ nextCursor: subscription.nextCursor,
158
+ commitCount: subscription.commits.length,
159
+ changeCount,
160
+ snapshotCount,
161
+ snapshotRowCount,
162
+ };
163
+ }),
164
+ };
165
+ }
166
+ function countPullRows(response) {
167
+ return response.subscriptions.reduce((totalRows, subscription) => {
168
+ const commitRows = subscription.commits.reduce((totalChanges, commit) => totalChanges + commit.changes.length, 0);
169
+ const snapshotRows = subscription.snapshots?.reduce((totalSnapshotRows, snapshot) => totalSnapshotRows + snapshot.rows.length, 0) ?? 0;
170
+ return totalRows + commitRows + snapshotRows;
171
+ }, 0);
172
+ }
173
+ function encodePayloadSnapshot(value) {
174
+ try {
175
+ const serialized = JSON.stringify(value);
176
+ if (serialized.length <= MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES) {
177
+ return serialized;
178
+ }
179
+ return JSON.stringify({
180
+ truncated: true,
181
+ originalSizeBytes: serialized.length,
182
+ preview: serialized.slice(0, MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES),
183
+ });
184
+ }
185
+ catch {
186
+ return JSON.stringify({
187
+ truncated: false,
188
+ serializationError: 'Could not serialize payload snapshot',
189
+ });
190
+ }
191
+ }
192
+ function emitConsoleLiveEvent(emitter, type, data) {
193
+ if (!emitter)
194
+ return;
195
+ emitter.emit({
196
+ type,
197
+ timestamp: new Date().toISOString(),
198
+ data,
199
+ });
200
+ }
28
201
  export function createSyncRoutes(options) {
29
202
  const routes = new Hono();
30
203
  routes.onError((error, c) => {
@@ -45,6 +218,7 @@ export function createSyncRoutes(options) {
45
218
  const maxPullLimitSnapshotRows = config.maxPullLimitSnapshotRows ?? 5000;
46
219
  const maxPullMaxSnapshotPages = config.maxPullMaxSnapshotPages ?? 10;
47
220
  const maxOperationsPerPush = config.maxOperationsPerPush ?? 200;
221
+ const consoleLiveEmitter = options.consoleLiveEmitter;
48
222
  // -------------------------------------------------------------------------
49
223
  // Optional WebSocket manager (scope-key based wake-ups)
50
224
  // -------------------------------------------------------------------------
@@ -89,18 +263,59 @@ export function createSyncRoutes(options) {
89
263
  realtimeUnsubscribeMap.set(routes, unsubscribe);
90
264
  }
91
265
  const recordRequestEvent = async (event) => {
266
+ let payloadRef = event.payloadRef ?? null;
267
+ if (event.payloadSnapshot) {
268
+ const nextPayloadRef = payloadRef ?? createOpaqueId('payload');
269
+ const nowIso = new Date().toISOString();
270
+ try {
271
+ await sql `
272
+ INSERT INTO sync_request_payloads (
273
+ payload_ref, partition_id, request_payload, response_payload, created_at
274
+ ) VALUES (
275
+ ${nextPayloadRef}, ${event.partitionId},
276
+ ${encodePayloadSnapshot(event.payloadSnapshot.request)},
277
+ ${encodePayloadSnapshot(event.payloadSnapshot.response)},
278
+ ${nowIso}
279
+ )
280
+ ON CONFLICT (payload_ref) DO UPDATE SET
281
+ partition_id = EXCLUDED.partition_id,
282
+ request_payload = EXCLUDED.request_payload,
283
+ response_payload = EXCLUDED.response_payload,
284
+ created_at = EXCLUDED.created_at
285
+ `.execute(options.db);
286
+ payloadRef = nextPayloadRef;
287
+ }
288
+ catch (error) {
289
+ payloadRef = null;
290
+ logAsyncFailureOnce('sync.request_payload_record_failed', {
291
+ event: 'sync.request_payload_record_failed',
292
+ userId: event.actorId,
293
+ clientId: event.clientId,
294
+ requestEventType: event.eventType,
295
+ error: error instanceof Error ? error.message : String(error),
296
+ });
297
+ }
298
+ }
92
299
  const tablesValue = options.dialect.arrayToDb(event.tables ?? []);
300
+ const scopesSummaryValue = event.scopesSummary
301
+ ? JSON.stringify(event.scopesSummary)
302
+ : null;
93
303
  await sql `
94
304
  INSERT INTO sync_request_events (
95
- event_type, actor_id, client_id, status_code, outcome,
96
- duration_ms, commit_seq, operation_count, row_count,
97
- tables, error_message, transport_path
305
+ partition_id, request_id, trace_id, span_id,
306
+ event_type, sync_path, actor_id, client_id, transport_path,
307
+ status_code, outcome, response_status, error_code,
308
+ duration_ms, commit_seq, operation_count, row_count, subscription_count,
309
+ scopes_summary, tables, error_message, payload_ref
98
310
  ) VALUES (
99
- ${event.eventType}, ${event.actorId}, ${event.clientId},
100
- ${event.statusCode}, ${event.outcome}, ${event.durationMs},
101
- ${event.commitSeq ?? null}, ${event.operationCount ?? null},
102
- ${event.rowCount ?? null}, ${tablesValue}, ${event.errorMessage ?? null},
103
- ${event.transportPath}
311
+ ${event.partitionId}, ${event.requestId}, ${event.traceId ?? null},
312
+ ${event.spanId ?? null}, ${event.eventType}, ${event.syncPath},
313
+ ${event.actorId}, ${event.clientId}, ${event.transportPath},
314
+ ${event.statusCode}, ${event.outcome}, ${event.responseStatus},
315
+ ${event.errorCode ?? null}, ${event.durationMs}, ${event.commitSeq ?? null},
316
+ ${event.operationCount ?? null}, ${event.rowCount ?? null},
317
+ ${event.subscriptionCount ?? null}, ${scopesSummaryValue}, ${tablesValue},
318
+ ${event.errorMessage ?? null}, ${payloadRef}
104
319
  )
105
320
  `.execute(options.db);
106
321
  };
@@ -224,6 +439,8 @@ export function createSyncRoutes(options) {
224
439
  const partitionId = auth.partitionId ?? 'default';
225
440
  const body = c.req.valid('json');
226
441
  const clientId = body.clientId;
442
+ const requestId = readRequestId(c);
443
+ const traceContext = readTraceContext(c);
227
444
  let pushResponse;
228
445
  let pullResponse;
229
446
  // --- Push phase ---
@@ -259,16 +476,48 @@ export function createSyncRoutes(options) {
259
476
  commitSeq: pushed.response.commitSeq,
260
477
  });
261
478
  recordRequestEventInBackground({
479
+ partitionId,
480
+ requestId,
481
+ traceId: traceContext.traceId,
482
+ spanId: traceContext.spanId,
262
483
  eventType: 'push',
484
+ syncPath: 'http-combined',
263
485
  actorId: auth.actorId,
264
486
  clientId,
265
487
  transportPath: readTransportPath(c),
266
488
  statusCode: 200,
267
489
  outcome: pushed.response.status,
490
+ responseStatus: normalizeResponseStatus(200, pushed.response.status),
268
491
  durationMs: pushDurationMs,
492
+ errorCode: firstPushErrorCode(pushed.response.results),
269
493
  commitSeq: pushed.response.commitSeq,
270
494
  operationCount: pushOps.length,
271
495
  tables: pushed.affectedTables,
496
+ payloadSnapshot: {
497
+ request: {
498
+ clientId,
499
+ clientCommitId: body.push.clientCommitId,
500
+ schemaVersion: body.push.schemaVersion,
501
+ operations: body.push.operations,
502
+ },
503
+ response: pushed.response,
504
+ },
505
+ });
506
+ emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
507
+ partitionId,
508
+ requestId,
509
+ traceId: traceContext.traceId,
510
+ spanId: traceContext.spanId,
511
+ actorId: auth.actorId,
512
+ clientId,
513
+ transportPath: readTransportPath(c),
514
+ syncPath: 'http-combined',
515
+ outcome: pushed.response.status,
516
+ statusCode: 200,
517
+ durationMs: pushDurationMs,
518
+ commitSeq: pushed.response.commitSeq ?? null,
519
+ operationCount: pushOps.length,
520
+ tables: pushed.affectedTables,
272
521
  });
273
522
  // WS notifications
274
523
  if (wsConnectionManager &&
@@ -301,6 +550,17 @@ export function createSyncRoutes(options) {
301
550
  }
302
551
  }
303
552
  }
553
+ if (pushed.response.ok === true &&
554
+ pushed.response.status === 'applied' &&
555
+ typeof pushed.response.commitSeq === 'number') {
556
+ emitConsoleLiveEvent(consoleLiveEmitter, 'commit', {
557
+ partitionId,
558
+ commitSeq: pushed.response.commitSeq,
559
+ actorId: auth.actorId,
560
+ clientId,
561
+ affectedTables: pushed.affectedTables,
562
+ });
563
+ }
304
564
  pushResponse = pushed.response;
305
565
  }
306
566
  // --- Pull phase ---
@@ -363,7 +623,17 @@ export function createSyncRoutes(options) {
363
623
  actorId: auth.actorId,
364
624
  cursor: pullResult.clientCursor,
365
625
  effectiveScopes: pullResult.effectiveScopes,
366
- }).catch((error) => {
626
+ })
627
+ .then(() => {
628
+ emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
629
+ action: 'cursor_recorded',
630
+ partitionId,
631
+ actorId: auth.actorId,
632
+ clientId,
633
+ cursor: pullResult.clientCursor,
634
+ });
635
+ })
636
+ .catch((error) => {
367
637
  logAsyncFailureOnce('sync.client_cursor_record_failed', {
368
638
  event: 'sync.client_cursor_record_failed',
369
639
  userId: auth.actorId,
@@ -381,13 +651,55 @@ export function createSyncRoutes(options) {
381
651
  clientCursor: pullResult.clientCursor,
382
652
  });
383
653
  recordRequestEventInBackground({
654
+ partitionId,
655
+ requestId,
656
+ traceId: traceContext.traceId,
657
+ spanId: traceContext.spanId,
384
658
  eventType: 'pull',
659
+ syncPath: 'http-combined',
385
660
  actorId: auth.actorId,
386
661
  clientId,
387
662
  transportPath: readTransportPath(c),
388
663
  statusCode: 200,
389
664
  outcome: 'applied',
665
+ responseStatus: normalizeResponseStatus(200, 'applied'),
390
666
  durationMs: pullDurationMs,
667
+ rowCount: countPullRows(pullResult.response),
668
+ subscriptionCount: request.subscriptions.length,
669
+ scopesSummary: summarizeScopeValues(pullResult.effectiveScopes),
670
+ payloadSnapshot: {
671
+ request: {
672
+ clientId,
673
+ limitCommits: request.limitCommits,
674
+ limitSnapshotRows: request.limitSnapshotRows,
675
+ maxSnapshotPages: request.maxSnapshotPages,
676
+ dedupeRows: request.dedupeRows,
677
+ subscriptions: request.subscriptions.map((subscription) => ({
678
+ id: subscription.id,
679
+ table: subscription.table,
680
+ scopes: subscription.scopes,
681
+ cursor: subscription.cursor,
682
+ bootstrapState: subscription.bootstrapState,
683
+ })),
684
+ },
685
+ response: summarizePullResponse(pullResult.response),
686
+ },
687
+ });
688
+ emitConsoleLiveEvent(consoleLiveEmitter, 'pull', {
689
+ partitionId,
690
+ requestId,
691
+ traceId: traceContext.traceId,
692
+ spanId: traceContext.spanId,
693
+ actorId: auth.actorId,
694
+ clientId,
695
+ transportPath: readTransportPath(c),
696
+ syncPath: 'http-combined',
697
+ outcome: 'applied',
698
+ statusCode: 200,
699
+ durationMs: pullDurationMs,
700
+ rowCount: countPullRows(pullResult.response),
701
+ subscriptionCount: request.subscriptions.length,
702
+ clientCursor: pullResult.clientCursor,
391
703
  });
392
704
  pullResponse = pullResult.response;
393
705
  }
@@ -567,6 +879,14 @@ export function createSyncRoutes(options) {
567
879
  connRef = conn;
568
880
  unregister = wsConnectionManager.register(conn, initialScopeKeys);
569
881
  conn.sendHeartbeat();
882
+ emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
883
+ action: 'realtime_connected',
884
+ actorId: auth.actorId,
885
+ clientId,
886
+ partitionId,
887
+ transportPath: realtimeTransportPath,
888
+ scopeCount: initialScopeKeys.length,
889
+ });
570
890
  },
571
891
  onClose(_evt, _ws) {
572
892
  unregister?.();
@@ -576,6 +896,12 @@ export function createSyncRoutes(options) {
576
896
  event: 'sync.realtime.disconnect',
577
897
  userId: auth.actorId,
578
898
  });
899
+ emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
900
+ action: 'realtime_disconnected',
901
+ actorId: auth.actorId,
902
+ clientId,
903
+ partitionId,
904
+ });
579
905
  },
580
906
  onError(_evt, _ws) {
581
907
  unregister?.();
@@ -585,6 +911,12 @@ export function createSyncRoutes(options) {
585
911
  event: 'sync.realtime.disconnect',
586
912
  userId: auth.actorId,
587
913
  });
914
+ emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
915
+ action: 'realtime_error',
916
+ actorId: auth.actorId,
917
+ clientId,
918
+ partitionId,
919
+ });
588
920
  },
589
921
  onMessage(evt, _ws) {
590
922
  if (!connRef)
@@ -668,10 +1000,13 @@ export function createSyncRoutes(options) {
668
1000
  const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
669
1001
  if (!requestId)
670
1002
  return;
1003
+ const traceContext = readTraceContextFromMessage(msg);
1004
+ const timer = createSyncTimer();
671
1005
  try {
672
1006
  // Validate the push payload
673
1007
  const parsed = SyncPushRequestSchema.omit({ clientId: true }).safeParse(msg);
674
1008
  if (!parsed.success) {
1009
+ const invalidDurationMs = timer();
675
1010
  conn.sendPushResponse({
676
1011
  requestId,
677
1012
  ok: false,
@@ -680,10 +1015,50 @@ export function createSyncRoutes(options) {
680
1015
  { opIndex: 0, status: 'error', error: 'Invalid push payload' },
681
1016
  ],
682
1017
  });
1018
+ recordRequestEventInBackground({
1019
+ partitionId,
1020
+ requestId,
1021
+ traceId: traceContext.traceId,
1022
+ spanId: traceContext.spanId,
1023
+ eventType: 'push',
1024
+ syncPath: 'ws-push',
1025
+ actorId,
1026
+ clientId,
1027
+ transportPath: conn.transportPath,
1028
+ statusCode: 400,
1029
+ outcome: 'rejected',
1030
+ responseStatus: normalizeResponseStatus(400, 'rejected'),
1031
+ durationMs: invalidDurationMs,
1032
+ errorCode: 'INVALID_PUSH_PAYLOAD',
1033
+ errorMessage: 'Invalid push payload',
1034
+ payloadSnapshot: {
1035
+ request: msg,
1036
+ response: {
1037
+ ok: false,
1038
+ status: 'rejected',
1039
+ reason: 'invalid_push_payload',
1040
+ },
1041
+ },
1042
+ });
1043
+ emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
1044
+ partitionId,
1045
+ requestId,
1046
+ traceId: traceContext.traceId,
1047
+ spanId: traceContext.spanId,
1048
+ actorId,
1049
+ clientId,
1050
+ transportPath: conn.transportPath,
1051
+ syncPath: 'ws-push',
1052
+ outcome: 'rejected',
1053
+ statusCode: 400,
1054
+ durationMs: invalidDurationMs,
1055
+ errorCode: 'INVALID_PUSH_PAYLOAD',
1056
+ });
683
1057
  return;
684
1058
  }
685
1059
  const pushOps = parsed.data.operations ?? [];
686
1060
  if (pushOps.length > maxOperationsPerPush) {
1061
+ const rejectedDurationMs = timer();
687
1062
  conn.sendPushResponse({
688
1063
  requestId,
689
1064
  ok: false,
@@ -696,9 +1071,54 @@ export function createSyncRoutes(options) {
696
1071
  },
697
1072
  ],
698
1073
  });
1074
+ recordRequestEventInBackground({
1075
+ partitionId,
1076
+ requestId,
1077
+ traceId: traceContext.traceId,
1078
+ spanId: traceContext.spanId,
1079
+ eventType: 'push',
1080
+ syncPath: 'ws-push',
1081
+ actorId,
1082
+ clientId,
1083
+ transportPath: conn.transportPath,
1084
+ statusCode: 400,
1085
+ outcome: 'rejected',
1086
+ responseStatus: normalizeResponseStatus(400, 'rejected'),
1087
+ durationMs: rejectedDurationMs,
1088
+ errorCode: 'MAX_OPERATIONS_EXCEEDED',
1089
+ errorMessage: `Maximum ${maxOperationsPerPush} operations per push`,
1090
+ operationCount: pushOps.length,
1091
+ payloadSnapshot: {
1092
+ request: {
1093
+ clientId,
1094
+ clientCommitId: parsed.data.clientCommitId,
1095
+ schemaVersion: parsed.data.schemaVersion,
1096
+ operations: parsed.data.operations,
1097
+ },
1098
+ response: {
1099
+ ok: false,
1100
+ status: 'rejected',
1101
+ reason: 'max_operations_exceeded',
1102
+ },
1103
+ },
1104
+ });
1105
+ emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
1106
+ partitionId,
1107
+ requestId,
1108
+ traceId: traceContext.traceId,
1109
+ spanId: traceContext.spanId,
1110
+ actorId,
1111
+ clientId,
1112
+ transportPath: conn.transportPath,
1113
+ syncPath: 'ws-push',
1114
+ outcome: 'rejected',
1115
+ statusCode: 400,
1116
+ durationMs: rejectedDurationMs,
1117
+ operationCount: pushOps.length,
1118
+ errorCode: 'MAX_OPERATIONS_EXCEEDED',
1119
+ });
699
1120
  return;
700
1121
  }
701
- const timer = createSyncTimer();
702
1122
  const pushed = await pushCommit({
703
1123
  db: options.db,
704
1124
  dialect: options.dialect,
@@ -722,16 +1142,48 @@ export function createSyncRoutes(options) {
722
1142
  commitSeq: pushed.response.commitSeq,
723
1143
  });
724
1144
  recordRequestEventInBackground({
1145
+ partitionId,
1146
+ requestId,
1147
+ traceId: traceContext.traceId,
1148
+ spanId: traceContext.spanId,
725
1149
  eventType: 'push',
1150
+ syncPath: 'ws-push',
726
1151
  actorId,
727
1152
  clientId,
728
1153
  transportPath: conn.transportPath,
729
1154
  statusCode: 200,
730
1155
  outcome: pushed.response.status,
1156
+ responseStatus: normalizeResponseStatus(200, pushed.response.status),
731
1157
  durationMs: pushDurationMs,
1158
+ errorCode: firstPushErrorCode(pushed.response.results),
732
1159
  commitSeq: pushed.response.commitSeq,
733
1160
  operationCount: pushOps.length,
734
1161
  tables: pushed.affectedTables,
1162
+ payloadSnapshot: {
1163
+ request: {
1164
+ clientId,
1165
+ clientCommitId: parsed.data.clientCommitId,
1166
+ schemaVersion: parsed.data.schemaVersion,
1167
+ operations: parsed.data.operations,
1168
+ },
1169
+ response: pushed.response,
1170
+ },
1171
+ });
1172
+ emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
1173
+ partitionId,
1174
+ requestId,
1175
+ traceId: traceContext.traceId,
1176
+ spanId: traceContext.spanId,
1177
+ actorId,
1178
+ clientId,
1179
+ transportPath: conn.transportPath,
1180
+ syncPath: 'ws-push',
1181
+ outcome: pushed.response.status,
1182
+ statusCode: 200,
1183
+ durationMs: pushDurationMs,
1184
+ commitSeq: pushed.response.commitSeq ?? null,
1185
+ operationCount: pushOps.length,
1186
+ tables: pushed.affectedTables,
735
1187
  });
736
1188
  // WS notifications to other clients
737
1189
  if (wsConnectionManager &&
@@ -764,6 +1216,17 @@ export function createSyncRoutes(options) {
764
1216
  }
765
1217
  }
766
1218
  }
1219
+ if (pushed.response.ok === true &&
1220
+ pushed.response.status === 'applied' &&
1221
+ typeof pushed.response.commitSeq === 'number') {
1222
+ emitConsoleLiveEvent(consoleLiveEmitter, 'commit', {
1223
+ partitionId,
1224
+ commitSeq: pushed.response.commitSeq,
1225
+ actorId,
1226
+ clientId,
1227
+ affectedTables: pushed.affectedTables,
1228
+ });
1229
+ }
767
1230
  conn.sendPushResponse({
768
1231
  requestId,
769
1232
  ok: pushed.response.ok,
@@ -773,6 +1236,7 @@ export function createSyncRoutes(options) {
773
1236
  });
774
1237
  }
775
1238
  catch (err) {
1239
+ const failedDurationMs = timer();
776
1240
  captureSyncException(err, {
777
1241
  event: 'sync.realtime.push_failed',
778
1242
  requestId,
@@ -781,6 +1245,46 @@ export function createSyncRoutes(options) {
781
1245
  partitionId,
782
1246
  });
783
1247
  const message = err instanceof Error ? err.message : 'Internal server error';
1248
+ recordRequestEventInBackground({
1249
+ partitionId,
1250
+ requestId,
1251
+ traceId: traceContext.traceId,
1252
+ spanId: traceContext.spanId,
1253
+ eventType: 'push',
1254
+ syncPath: 'ws-push',
1255
+ actorId,
1256
+ clientId,
1257
+ transportPath: conn.transportPath,
1258
+ statusCode: 500,
1259
+ outcome: 'error',
1260
+ responseStatus: normalizeResponseStatus(500, 'error'),
1261
+ durationMs: failedDurationMs,
1262
+ errorCode: 'INTERNAL_SERVER_ERROR',
1263
+ errorMessage: message,
1264
+ payloadSnapshot: {
1265
+ request: msg,
1266
+ response: {
1267
+ ok: false,
1268
+ status: 'rejected',
1269
+ reason: 'internal_server_error',
1270
+ message,
1271
+ },
1272
+ },
1273
+ });
1274
+ emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
1275
+ partitionId,
1276
+ requestId,
1277
+ traceId: traceContext.traceId,
1278
+ spanId: traceContext.spanId,
1279
+ actorId,
1280
+ clientId,
1281
+ transportPath: conn.transportPath,
1282
+ syncPath: 'ws-push',
1283
+ outcome: 'error',
1284
+ statusCode: 500,
1285
+ durationMs: failedDurationMs,
1286
+ errorCode: 'INTERNAL_SERVER_ERROR',
1287
+ });
784
1288
  conn.sendPushResponse({
785
1289
  requestId,
786
1290
  ok: false,