@syncular/server-hono 0.0.4-25 → 0.0.6-100

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.
Files changed (57) hide show
  1. package/README.md +6 -1
  2. package/dist/console/gateway.d.ts +3 -1
  3. package/dist/console/gateway.d.ts.map +1 -1
  4. package/dist/console/gateway.js +227 -42
  5. package/dist/console/gateway.js.map +1 -1
  6. package/dist/console/index.d.ts +2 -0
  7. package/dist/console/index.d.ts.map +1 -1
  8. package/dist/console/index.js +2 -0
  9. package/dist/console/index.js.map +1 -1
  10. package/dist/console/routes.d.ts +3 -97
  11. package/dist/console/routes.d.ts.map +1 -1
  12. package/dist/console/routes.js +516 -81
  13. package/dist/console/routes.js.map +1 -1
  14. package/dist/console/schemas.d.ts +29 -0
  15. package/dist/console/schemas.d.ts.map +1 -1
  16. package/dist/console/schemas.js +22 -0
  17. package/dist/console/schemas.js.map +1 -1
  18. package/dist/console/types.d.ts +175 -0
  19. package/dist/console/types.d.ts.map +1 -0
  20. package/dist/console/types.js +2 -0
  21. package/dist/console/types.js.map +1 -0
  22. package/dist/console/ui.d.ts +38 -0
  23. package/dist/console/ui.d.ts.map +1 -0
  24. package/dist/console/ui.js +43 -0
  25. package/dist/console/ui.js.map +1 -0
  26. package/dist/create-server.d.ts +17 -34
  27. package/dist/create-server.d.ts.map +1 -1
  28. package/dist/create-server.js +26 -26
  29. package/dist/create-server.js.map +1 -1
  30. package/dist/proxy/connection-manager.d.ts +3 -3
  31. package/dist/proxy/connection-manager.d.ts.map +1 -1
  32. package/dist/proxy/routes.d.ts +4 -4
  33. package/dist/proxy/routes.d.ts.map +1 -1
  34. package/dist/proxy/routes.js +1 -1
  35. package/dist/routes.d.ts +33 -9
  36. package/dist/routes.d.ts.map +1 -1
  37. package/dist/routes.js +153 -70
  38. package/dist/routes.js.map +1 -1
  39. package/package.json +21 -6
  40. package/src/__tests__/blob-routes.test.ts +424 -0
  41. package/src/__tests__/console-gateway-live-routes.test.ts +54 -3
  42. package/src/__tests__/console-routes.test.ts +161 -7
  43. package/src/__tests__/console-ui.test.ts +114 -0
  44. package/src/__tests__/create-server.test.ts +233 -10
  45. package/src/__tests__/pull-chunk-storage.test.ts +6 -2
  46. package/src/__tests__/realtime-bridge.test.ts +6 -2
  47. package/src/__tests__/sync-rate-limit-routing.test.ts +6 -2
  48. package/src/console/gateway.ts +286 -54
  49. package/src/console/index.ts +2 -0
  50. package/src/console/routes.ts +663 -199
  51. package/src/console/schemas.ts +29 -0
  52. package/src/console/types.ts +185 -0
  53. package/src/console/ui.ts +100 -0
  54. package/src/create-server.ts +56 -53
  55. package/src/proxy/connection-manager.ts +3 -3
  56. package/src/proxy/routes.ts +4 -4
  57. package/src/routes.ts +225 -96
package/dist/routes.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * - GET /snapshot-chunks/:chunkId (download encoded snapshot chunks)
7
7
  * - GET /realtime (optional WebSocket "wake up" notifications)
8
8
  */
9
- import type { ServerSyncDialect, ServerTableHandler, SnapshotChunkStorage, SyncCoreDb, SyncRealtimeBroadcaster } from '@syncular/server';
9
+ import type { ScopeCacheBackend, ServerSyncDialect, ServerTableHandler, SnapshotChunkStorage, SqlFamily, SyncCoreDb, SyncRealtimeBroadcaster, SyncServerAuth } from '@syncular/server';
10
10
  import { type CompactOptions, type PruneOptions } from '@syncular/server';
11
11
  import type { Context } from 'hono';
12
12
  import { Hono } from 'hono';
@@ -14,9 +14,7 @@ import type { UpgradeWebSocket } from 'hono/ws';
14
14
  import { type Kysely } from 'kysely';
15
15
  import { type SyncRateLimitConfig } from './rate-limit';
16
16
  import { WebSocketConnectionManager } from './ws';
17
- export interface SyncAuthResult {
18
- actorId: string;
19
- partitionId?: string;
17
+ export interface SyncAuthResult extends SyncServerAuth {
20
18
  }
21
19
  /**
22
20
  * WebSocket configuration for realtime sync.
@@ -67,6 +65,22 @@ export interface SyncRoutesConfigWithRateLimit {
67
65
  * Default: 200
68
66
  */
69
67
  maxOperationsPerPush?: number;
68
+ /**
69
+ * Request/response payload snapshots recorded for console inspection.
70
+ */
71
+ requestPayloadSnapshots?: {
72
+ /**
73
+ * Enable payload snapshot storage in `sync_request_payloads`.
74
+ * Default: true when console event recording is enabled.
75
+ */
76
+ enabled?: boolean;
77
+ /**
78
+ * Max serialized payload size in bytes per request/response snapshot.
79
+ * Larger payloads are truncated with metadata.
80
+ * Default: 128 KiB.
81
+ */
82
+ maxBytes?: number;
83
+ };
70
84
  /**
71
85
  * Rate limiting configuration.
72
86
  * Set to false to disable all rate limiting.
@@ -106,11 +120,11 @@ export interface SyncRoutesConfigWithRateLimit {
106
120
  instanceId?: string;
107
121
  };
108
122
  }
109
- export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
123
+ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb, Auth extends SyncAuthResult = SyncAuthResult, F extends SqlFamily = SqlFamily> {
110
124
  db: Kysely<DB>;
111
- dialect: ServerSyncDialect;
112
- handlers: ServerTableHandler<DB>[];
113
- authenticate: (c: Context) => Promise<SyncAuthResult | null>;
125
+ dialect: ServerSyncDialect<F>;
126
+ handlers: ServerTableHandler<DB, Auth>[];
127
+ authenticate: (c: Context) => Promise<Auth | null>;
114
128
  sync?: SyncRoutesConfigWithRateLimit;
115
129
  wsConnectionManager?: WebSocketConnectionManager;
116
130
  /**
@@ -119,6 +133,11 @@ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
119
133
  * (S3, R2, etc.) instead of inline in the database.
120
134
  */
121
135
  chunkStorage?: SnapshotChunkStorage;
136
+ /**
137
+ * Optional scope cache backend for resolveScopes() results.
138
+ * Request-local memoization is always applied for every pull.
139
+ */
140
+ scopeCache?: ScopeCacheBackend;
122
141
  /**
123
142
  * Optional live emitter for console websocket activity feed.
124
143
  * When provided, sync lifecycle events are published to `/console/events/live`.
@@ -130,8 +149,13 @@ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
130
149
  data: Record<string, unknown>;
131
150
  }): void;
132
151
  };
152
+ /**
153
+ * Optional console schema readiness promise.
154
+ * When provided, request-event recording waits for this promise before writing.
155
+ */
156
+ consoleSchemaReady?: Promise<void>;
133
157
  }
134
- export declare function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(options: CreateSyncRoutesOptions<DB>): Hono;
158
+ export declare function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb, Auth extends SyncAuthResult = SyncAuthResult, F extends SqlFamily = SqlFamily>(options: CreateSyncRoutesOptions<DB, Auth, F>): Hono;
135
159
  export declare function getSyncWebSocketConnectionManager(routes: Hono): WebSocketConnectionManager | undefined;
136
160
  export declare function getSyncRealtimeUnsubscribe(routes: Hono): (() => void) | undefined;
137
161
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
package/dist/routes.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * - GET /snapshot-chunks/:chunkId (download encoded snapshot chunks)
7
7
  * - GET /realtime (optional WebSocket "wake up" notifications)
8
8
  */
9
- import { captureSyncException, createSyncTimer, ErrorResponseSchema, logSyncEvent, SyncCombinedRequestSchema, SyncCombinedResponseSchema, SyncPushRequestSchema, } from '@syncular/core';
10
- import { InvalidSubscriptionScopeError, pull, pushCommit, readSnapshotChunk, recordClientCursor, TableRegistry, } from '@syncular/server';
9
+ import { captureSyncException, countSyncMetric, createSyncTimer, distributionSyncMetric, ErrorResponseSchema, logSyncEvent, SyncCombinedRequestSchema, SyncCombinedResponseSchema, SyncPushRequestSchema, } from '@syncular/core';
10
+ import { createServerHandlerCollection, InvalidSubscriptionScopeError, pull, pushCommit, readSnapshotChunk, recordClientCursor, } from '@syncular/server';
11
11
  import { Hono } from 'hono';
12
12
  import { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
13
13
  import { sql, } from 'kysely';
@@ -25,7 +25,7 @@ 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;
28
+ const DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES = 128 * 1024;
29
29
  function createOpaqueId(prefix) {
30
30
  const randomPart = typeof crypto !== 'undefined' && 'randomUUID' in crypto
31
31
  ? crypto.randomUUID()
@@ -64,6 +64,15 @@ function parseSentryTraceHeader(sentryTrace) {
64
64
  return null;
65
65
  return { traceId, spanId };
66
66
  }
67
+ function readPositiveInteger(value, fallback) {
68
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
69
+ return fallback;
70
+ }
71
+ if (value <= 0) {
72
+ return fallback;
73
+ }
74
+ return Math.floor(value);
75
+ }
67
76
  function readTraceContext(c) {
68
77
  const traceparent = parseW3cTraceparent(c.req.header('traceparent'));
69
78
  if (traceparent)
@@ -170,16 +179,16 @@ function countPullRows(response) {
170
179
  return totalRows + commitRows + snapshotRows;
171
180
  }, 0);
172
181
  }
173
- function encodePayloadSnapshot(value) {
182
+ function encodePayloadSnapshot(value, maxBytes) {
174
183
  try {
175
184
  const serialized = JSON.stringify(value);
176
- if (serialized.length <= MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES) {
185
+ if (serialized.length <= maxBytes) {
177
186
  return serialized;
178
187
  }
179
188
  return JSON.stringify({
180
189
  truncated: true,
181
190
  originalSizeBytes: serialized.length,
182
- preview: serialized.slice(0, MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES),
191
+ preview: serialized.slice(0, maxBytes),
183
192
  });
184
193
  }
185
194
  catch {
@@ -208,10 +217,7 @@ export function createSyncRoutes(options) {
208
217
  });
209
218
  return c.text('Internal Server Error', 500);
210
219
  });
211
- const handlerRegistry = new TableRegistry();
212
- for (const handler of options.handlers) {
213
- handlerRegistry.register(handler);
214
- }
220
+ const handlerRegistry = createServerHandlerCollection(options.handlers);
215
221
  const config = options.sync ?? {};
216
222
  const maxPullLimitCommits = config.maxPullLimitCommits ?? 100;
217
223
  const maxSubscriptionsPerPull = config.maxSubscriptionsPerPull ?? 200;
@@ -221,6 +227,21 @@ export function createSyncRoutes(options) {
221
227
  const consoleLiveEmitter = options.consoleLiveEmitter;
222
228
  const shouldEmitConsoleLiveEvents = consoleLiveEmitter !== undefined;
223
229
  const shouldRecordRequestEvents = shouldEmitConsoleLiveEvents;
230
+ const shouldCaptureRequestPayloadSnapshots = shouldRecordRequestEvents &&
231
+ config.requestPayloadSnapshots?.enabled !== false;
232
+ const requestPayloadSnapshotMaxBytes = readPositiveInteger(config.requestPayloadSnapshots?.maxBytes, DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES);
233
+ const consoleSchemaReadyBase = shouldRecordRequestEvents
234
+ ? (options.consoleSchemaReady ??
235
+ options.dialect.ensureConsoleSchema?.(options.db) ??
236
+ Promise.resolve())
237
+ : Promise.resolve();
238
+ const consoleSchemaReady = consoleSchemaReadyBase.catch((error) => {
239
+ logSyncEvent({
240
+ event: 'sync.console_schema_ready_failed',
241
+ error: error instanceof Error ? error.message : String(error),
242
+ });
243
+ throw error;
244
+ });
224
245
  // -------------------------------------------------------------------------
225
246
  // Optional WebSocket manager (scope-key based wake-ups)
226
247
  // -------------------------------------------------------------------------
@@ -275,8 +296,8 @@ export function createSyncRoutes(options) {
275
296
  payload_ref, partition_id, request_payload, response_payload, created_at
276
297
  ) VALUES (
277
298
  ${nextPayloadRef}, ${event.partitionId},
278
- ${encodePayloadSnapshot(event.payloadSnapshot.request)},
279
- ${encodePayloadSnapshot(event.payloadSnapshot.response)},
299
+ ${encodePayloadSnapshot(event.payloadSnapshot.request, requestPayloadSnapshotMaxBytes)},
300
+ ${encodePayloadSnapshot(event.payloadSnapshot.response, requestPayloadSnapshotMaxBytes)},
280
301
  ${nowIso}
281
302
  )
282
303
  ON CONFLICT (payload_ref) DO UPDATE SET
@@ -325,7 +346,9 @@ export function createSyncRoutes(options) {
325
346
  if (!shouldRecordRequestEvents)
326
347
  return;
327
348
  const resolvedEvent = typeof event === 'function' ? event() : event;
328
- void recordRequestEvent(resolvedEvent).catch((error) => {
349
+ void consoleSchemaReady
350
+ .then(() => recordRequestEvent(resolvedEvent))
351
+ .catch((error) => {
329
352
  logAsyncFailureOnce('sync.request_event_record_failed', {
330
353
  event: 'sync.request_event_record_failed',
331
354
  userId: resolvedEvent.actorId,
@@ -463,8 +486,7 @@ export function createSyncRoutes(options) {
463
486
  db: options.db,
464
487
  dialect: options.dialect,
465
488
  handlers: handlerRegistry,
466
- actorId: auth.actorId,
467
- partitionId,
489
+ auth,
468
490
  request: {
469
491
  clientId,
470
492
  clientCommitId: pushBody.clientCommitId,
@@ -499,15 +521,17 @@ export function createSyncRoutes(options) {
499
521
  commitSeq: pushed.response.commitSeq,
500
522
  operationCount: pushOps.length,
501
523
  tables: pushed.affectedTables,
502
- payloadSnapshot: {
503
- request: {
504
- clientId,
505
- clientCommitId: pushBody.clientCommitId,
506
- schemaVersion: pushBody.schemaVersion,
507
- operations: pushBody.operations,
508
- },
509
- response: pushed.response,
510
- },
524
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
525
+ ? {
526
+ request: {
527
+ clientId,
528
+ clientCommitId: pushBody.clientCommitId,
529
+ schemaVersion: pushBody.schemaVersion,
530
+ operations: pushBody.operations,
531
+ },
532
+ response: pushed.response,
533
+ }
534
+ : null,
511
535
  }));
512
536
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
513
537
  partitionId,
@@ -610,10 +634,10 @@ export function createSyncRoutes(options) {
610
634
  db: options.db,
611
635
  dialect: options.dialect,
612
636
  handlers: handlerRegistry,
613
- actorId: auth.actorId,
614
- partitionId,
637
+ auth,
615
638
  request,
616
639
  chunkStorage: options.chunkStorage,
640
+ scopeCache: options.scopeCache,
617
641
  });
618
642
  }
619
643
  catch (err) {
@@ -663,7 +687,7 @@ export function createSyncRoutes(options) {
663
687
  const scopesSummary = shouldRecordRequestEvents
664
688
  ? summarizeScopeValues(pullResult.effectiveScopes)
665
689
  : null;
666
- const payloadSnapshot = shouldRecordRequestEvents
690
+ const payloadSnapshot = shouldCaptureRequestPayloadSnapshots
667
691
  ? {
668
692
  request: {
669
693
  clientId,
@@ -887,6 +911,31 @@ export function createSyncRoutes(options) {
887
911
  logSyncEvent({ event: 'sync.realtime.connect', userId: auth.actorId });
888
912
  let unregister = null;
889
913
  let connRef = null;
914
+ const connectionCountBeforeUpgrade = wsConnectionManager.getConnectionCount(clientId);
915
+ let sessionStartedAtMs = null;
916
+ let sessionEnded = false;
917
+ const finishRealtimeSession = (reason) => {
918
+ if (sessionEnded)
919
+ return;
920
+ sessionEnded = true;
921
+ if (sessionStartedAtMs === null) {
922
+ return;
923
+ }
924
+ const durationMs = Math.max(0, Date.now() - sessionStartedAtMs);
925
+ countSyncMetric('sync.sessions.ended', 1, {
926
+ attributes: {
927
+ transportPath: realtimeTransportPath,
928
+ reason,
929
+ },
930
+ });
931
+ distributionSyncMetric('sync.sessions.duration_ms', durationMs, {
932
+ unit: 'millisecond',
933
+ attributes: {
934
+ transportPath: realtimeTransportPath,
935
+ reason,
936
+ },
937
+ });
938
+ };
890
939
  const upgradeWebSocket = websocketConfig.upgradeWebSocket;
891
940
  if (!upgradeWebSocket) {
892
941
  return c.json({ error: 'WEBSOCKET_NOT_CONFIGURED' }, 500);
@@ -899,6 +948,20 @@ export function createSyncRoutes(options) {
899
948
  transportPath: realtimeTransportPath,
900
949
  });
901
950
  connRef = conn;
951
+ sessionStartedAtMs = Date.now();
952
+ countSyncMetric('sync.sessions.started', 1, {
953
+ attributes: {
954
+ transportPath: realtimeTransportPath,
955
+ },
956
+ });
957
+ if (connectionCountBeforeUpgrade > 0) {
958
+ countSyncMetric('sync.transport.reconnects', 1, {
959
+ attributes: {
960
+ transportPath: realtimeTransportPath,
961
+ source: 'server',
962
+ },
963
+ });
964
+ }
902
965
  unregister = wsConnectionManager.register(conn, initialScopeKeys);
903
966
  conn.sendHeartbeat();
904
967
  emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', () => ({
@@ -914,6 +977,7 @@ export function createSyncRoutes(options) {
914
977
  unregister?.();
915
978
  unregister = null;
916
979
  connRef = null;
980
+ finishRealtimeSession('closed');
917
981
  logSyncEvent({
918
982
  event: 'sync.realtime.disconnect',
919
983
  userId: auth.actorId,
@@ -929,6 +993,7 @@ export function createSyncRoutes(options) {
929
993
  unregister?.();
930
994
  unregister = null;
931
995
  connRef = null;
996
+ finishRealtimeSession('error');
932
997
  logSyncEvent({
933
998
  event: 'sync.realtime.disconnect',
934
999
  userId: auth.actorId,
@@ -949,7 +1014,7 @@ export function createSyncRoutes(options) {
949
1014
  if (!msg || typeof msg !== 'object')
950
1015
  return;
951
1016
  if (msg.type === 'push') {
952
- void handleWsPush(msg, connRef, auth.actorId, partitionId, clientId);
1017
+ void handleWsPush(msg, connRef, auth, clientId);
953
1018
  return;
954
1019
  }
955
1020
  if (msg.type !== 'presence' || !msg.scopeKey)
@@ -1018,7 +1083,9 @@ export function createSyncRoutes(options) {
1018
1083
  return;
1019
1084
  wsConnectionManager.notifyScopeKeys(scopeKeys, commitSeq);
1020
1085
  }
1021
- async function handleWsPush(msg, conn, actorId, partitionId, clientId) {
1086
+ async function handleWsPush(msg, conn, auth, clientId) {
1087
+ const actorId = auth.actorId;
1088
+ const partitionId = auth.partitionId ?? 'default';
1022
1089
  const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
1023
1090
  if (!requestId)
1024
1091
  return;
@@ -1053,14 +1120,16 @@ export function createSyncRoutes(options) {
1053
1120
  durationMs: invalidDurationMs,
1054
1121
  errorCode: 'INVALID_PUSH_PAYLOAD',
1055
1122
  errorMessage: 'Invalid push payload',
1056
- payloadSnapshot: {
1057
- request: msg,
1058
- response: {
1059
- ok: false,
1060
- status: 'rejected',
1061
- reason: 'invalid_push_payload',
1062
- },
1063
- },
1123
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
1124
+ ? {
1125
+ request: msg,
1126
+ response: {
1127
+ ok: false,
1128
+ status: 'rejected',
1129
+ reason: 'invalid_push_payload',
1130
+ },
1131
+ }
1132
+ : null,
1064
1133
  }));
1065
1134
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1066
1135
  partitionId,
@@ -1110,19 +1179,21 @@ export function createSyncRoutes(options) {
1110
1179
  errorCode: 'MAX_OPERATIONS_EXCEEDED',
1111
1180
  errorMessage: `Maximum ${maxOperationsPerPush} operations per push`,
1112
1181
  operationCount: pushOps.length,
1113
- payloadSnapshot: {
1114
- request: {
1115
- clientId,
1116
- clientCommitId: parsed.data.clientCommitId,
1117
- schemaVersion: parsed.data.schemaVersion,
1118
- operations: parsed.data.operations,
1119
- },
1120
- response: {
1121
- ok: false,
1122
- status: 'rejected',
1123
- reason: 'max_operations_exceeded',
1124
- },
1125
- },
1182
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
1183
+ ? {
1184
+ request: {
1185
+ clientId,
1186
+ clientCommitId: parsed.data.clientCommitId,
1187
+ schemaVersion: parsed.data.schemaVersion,
1188
+ operations: parsed.data.operations,
1189
+ },
1190
+ response: {
1191
+ ok: false,
1192
+ status: 'rejected',
1193
+ reason: 'max_operations_exceeded',
1194
+ },
1195
+ }
1196
+ : null,
1126
1197
  }));
1127
1198
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1128
1199
  partitionId,
@@ -1145,8 +1216,7 @@ export function createSyncRoutes(options) {
1145
1216
  db: options.db,
1146
1217
  dialect: options.dialect,
1147
1218
  handlers: handlerRegistry,
1148
- actorId,
1149
- partitionId,
1219
+ auth,
1150
1220
  request: {
1151
1221
  clientId,
1152
1222
  clientCommitId: parsed.data.clientCommitId,
@@ -1181,15 +1251,17 @@ export function createSyncRoutes(options) {
1181
1251
  commitSeq: pushed.response.commitSeq,
1182
1252
  operationCount: pushOps.length,
1183
1253
  tables: pushed.affectedTables,
1184
- payloadSnapshot: {
1185
- request: {
1186
- clientId,
1187
- clientCommitId: parsed.data.clientCommitId,
1188
- schemaVersion: parsed.data.schemaVersion,
1189
- operations: parsed.data.operations,
1190
- },
1191
- response: pushed.response,
1192
- },
1254
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
1255
+ ? {
1256
+ request: {
1257
+ clientId,
1258
+ clientCommitId: parsed.data.clientCommitId,
1259
+ schemaVersion: parsed.data.schemaVersion,
1260
+ operations: parsed.data.operations,
1261
+ },
1262
+ response: pushed.response,
1263
+ }
1264
+ : null,
1193
1265
  }));
1194
1266
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1195
1267
  partitionId,
@@ -1207,6 +1279,15 @@ export function createSyncRoutes(options) {
1207
1279
  operationCount: pushOps.length,
1208
1280
  tables: pushed.affectedTables,
1209
1281
  }));
1282
+ const detectedConflicts = pushed.response.results.reduce((count, result) => count + (result.status === 'conflict' ? 1 : 0), 0);
1283
+ if (detectedConflicts > 0) {
1284
+ countSyncMetric('sync.conflicts.detected', detectedConflicts, {
1285
+ attributes: {
1286
+ syncPath: 'ws-push',
1287
+ transportPath: conn.transportPath,
1288
+ },
1289
+ });
1290
+ }
1210
1291
  // WS notifications to other clients
1211
1292
  if (wsConnectionManager &&
1212
1293
  pushed.response.ok === true &&
@@ -1283,15 +1364,17 @@ export function createSyncRoutes(options) {
1283
1364
  durationMs: failedDurationMs,
1284
1365
  errorCode: 'INTERNAL_SERVER_ERROR',
1285
1366
  errorMessage: message,
1286
- payloadSnapshot: {
1287
- request: msg,
1288
- response: {
1289
- ok: false,
1290
- status: 'rejected',
1291
- reason: 'internal_server_error',
1292
- message,
1293
- },
1294
- },
1367
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
1368
+ ? {
1369
+ request: msg,
1370
+ response: {
1371
+ ok: false,
1372
+ status: 'rejected',
1373
+ reason: 'internal_server_error',
1374
+ message,
1375
+ },
1376
+ }
1377
+ : null,
1295
1378
  }));
1296
1379
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1297
1380
  partitionId,