@juspay/neurolink 12.14.19 → 12.14.21
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/CHANGELOG.md +6 -2
- package/dist/browser/neurolink.min.js +370 -370
- package/dist/cli/commands/proxy.js +78 -46
- package/dist/proxy/bodyCaptureWorker.js +126 -13
- package/dist/proxy/requestLogger.d.ts +4 -1
- package/dist/proxy/requestLogger.js +10 -3
- package/dist/proxy/updateCoordinator.d.ts +13 -0
- package/dist/proxy/updateCoordinator.js +13 -0
- package/dist/types/generate.d.ts +9 -0
- package/dist/types/proxy.d.ts +13 -1
- package/dist/utils/fileDetector.d.ts +4 -0
- package/dist/utils/fileDetector.js +65 -52
- package/dist/utils/messageBuilder.js +143 -76
- package/package.json +1 -1
- package/scripts/observability/proxy-telemetry-check.mjs +306 -33
- package/scripts/observability/query-proxy-history.mjs +1 -1
|
@@ -7,6 +7,229 @@ import {
|
|
|
7
7
|
validateProxyTelemetryBackend,
|
|
8
8
|
} from "./proxy-telemetry-backend.mjs";
|
|
9
9
|
|
|
10
|
+
/** @param {string} kind @param {Record<string, unknown>} value @param {string | undefined} eventId */
|
|
11
|
+
function logicalMetadataIdentity(kind, value, eventId) {
|
|
12
|
+
if (kind === "request_final") {
|
|
13
|
+
return typeof value.requestId === "string" && value.requestId
|
|
14
|
+
? `request:${value.requestId}`
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
if (kind === "body_capture_index") {
|
|
18
|
+
return typeof value.captureId === "string" && value.captureId
|
|
19
|
+
? `capture:${value.captureId}`
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
if (kind === "telemetry_delivery") {
|
|
23
|
+
return typeof value.id === "string" && value.id
|
|
24
|
+
? `delivery:${value.id}`
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
kind === "lifecycle" &&
|
|
29
|
+
typeof value.processInstanceId === "string" &&
|
|
30
|
+
value.processInstanceId &&
|
|
31
|
+
Number.isSafeInteger(value.sequence)
|
|
32
|
+
) {
|
|
33
|
+
return `lifecycle:${value.processInstanceId}:${value.sequence}`;
|
|
34
|
+
}
|
|
35
|
+
return eventId ? `event:${eventId}` : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Collapse only exact OTLP retries. A reused event identity with changed data,
|
|
40
|
+
* or two producer events for one logical record, remains a hard diagnostic.
|
|
41
|
+
* @param {string} kind
|
|
42
|
+
* @param {Array<Record<string, unknown>>} rows
|
|
43
|
+
*/
|
|
44
|
+
function normalizeMetadataRows(kind, rows) {
|
|
45
|
+
const byEvent = new Map();
|
|
46
|
+
const byPhysicalRow = new Map();
|
|
47
|
+
const parsed = [];
|
|
48
|
+
const conflicts = [];
|
|
49
|
+
let exactRetryDuplicates = 0;
|
|
50
|
+
let missingEventIds = 0;
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
if (typeof row.body !== "string") {
|
|
53
|
+
throw new Error("Invalid stored metadata body");
|
|
54
|
+
}
|
|
55
|
+
const value = JSON.parse(row.body);
|
|
56
|
+
if (
|
|
57
|
+
!value ||
|
|
58
|
+
typeof value !== "object" ||
|
|
59
|
+
(kind === "telemetry_delivery"
|
|
60
|
+
? typeof value.id !== "string" ||
|
|
61
|
+
typeof value.at !== "string" ||
|
|
62
|
+
!Number.isFinite(Date.parse(value.at))
|
|
63
|
+
: typeof value.requestId !== "string")
|
|
64
|
+
) {
|
|
65
|
+
throw new Error("Stored telemetry metadata has an invalid schema");
|
|
66
|
+
}
|
|
67
|
+
const eventId =
|
|
68
|
+
typeof row.proxy_event_id === "string" && row.proxy_event_id
|
|
69
|
+
? row.proxy_event_id
|
|
70
|
+
: undefined;
|
|
71
|
+
const physicalIdentity = JSON.stringify([
|
|
72
|
+
row._timestamp,
|
|
73
|
+
row.service_instance_id,
|
|
74
|
+
row.request_id,
|
|
75
|
+
row.body,
|
|
76
|
+
]);
|
|
77
|
+
const priorEvent = eventId ? byEvent.get(eventId) : undefined;
|
|
78
|
+
if (priorEvent) {
|
|
79
|
+
if (priorEvent.physicalIdentity === physicalIdentity) {
|
|
80
|
+
exactRetryDuplicates++;
|
|
81
|
+
} else {
|
|
82
|
+
conflicts.push({
|
|
83
|
+
identity: `event:${eventId}`,
|
|
84
|
+
reason: "event_identity_conflict",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!eventId) {
|
|
90
|
+
missingEventIds++;
|
|
91
|
+
if (byPhysicalRow.has(physicalIdentity)) {
|
|
92
|
+
exactRetryDuplicates++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
byPhysicalRow.set(physicalIdentity, true);
|
|
96
|
+
} else {
|
|
97
|
+
byEvent.set(eventId, { physicalIdentity });
|
|
98
|
+
}
|
|
99
|
+
parsed.push({
|
|
100
|
+
eventId,
|
|
101
|
+
rawBody: row.body,
|
|
102
|
+
value: {
|
|
103
|
+
...value,
|
|
104
|
+
recordedAtMicroseconds: Number(row._timestamp),
|
|
105
|
+
...(eventId ? { proxyEventId: eventId } : {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const byLogicalIdentity = new Map();
|
|
111
|
+
const records = [];
|
|
112
|
+
let invalidLogicalIdentities = 0;
|
|
113
|
+
for (const item of parsed) {
|
|
114
|
+
const identity = logicalMetadataIdentity(kind, item.value, item.eventId);
|
|
115
|
+
if (!identity) {
|
|
116
|
+
invalidLogicalIdentities++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const prior = byLogicalIdentity.get(identity);
|
|
120
|
+
if (prior) {
|
|
121
|
+
conflicts.push({
|
|
122
|
+
identity,
|
|
123
|
+
reason:
|
|
124
|
+
prior.rawBody === item.rawBody
|
|
125
|
+
? "producer_duplicate"
|
|
126
|
+
: "logical_identity_conflict",
|
|
127
|
+
firstEventId: prior.eventId,
|
|
128
|
+
duplicateEventId: item.eventId,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
byLogicalIdentity.set(identity, item);
|
|
133
|
+
records.push(item.value);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
records,
|
|
137
|
+
diagnostics: {
|
|
138
|
+
rawRecords: rows.length,
|
|
139
|
+
logicalRecords: records.length,
|
|
140
|
+
exactRetryDuplicates,
|
|
141
|
+
missingEventIds,
|
|
142
|
+
invalidLogicalIdentities,
|
|
143
|
+
conflicts,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @param {Array<Record<string, unknown>>} rows */
|
|
149
|
+
function normalizeBodyChunkRows(rows) {
|
|
150
|
+
const byEvent = new Map();
|
|
151
|
+
const byPhysicalRow = new Map();
|
|
152
|
+
const parsed = [];
|
|
153
|
+
const conflicts = [];
|
|
154
|
+
let exactRetryDuplicates = 0;
|
|
155
|
+
let missingEventIds = 0;
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const eventId =
|
|
158
|
+
typeof row.proxy_event_id === "string" && row.proxy_event_id
|
|
159
|
+
? row.proxy_event_id
|
|
160
|
+
: undefined;
|
|
161
|
+
const physicalIdentity = JSON.stringify([
|
|
162
|
+
row._timestamp,
|
|
163
|
+
row.body_capture_id,
|
|
164
|
+
row.body_chunk_index,
|
|
165
|
+
row.body_chunk_count,
|
|
166
|
+
row.body,
|
|
167
|
+
]);
|
|
168
|
+
const priorEvent = eventId ? byEvent.get(eventId) : undefined;
|
|
169
|
+
if (priorEvent) {
|
|
170
|
+
if (priorEvent === physicalIdentity) {
|
|
171
|
+
exactRetryDuplicates++;
|
|
172
|
+
} else {
|
|
173
|
+
conflicts.push({
|
|
174
|
+
identity: `event:${eventId}`,
|
|
175
|
+
reason: "event_identity_conflict",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!eventId) {
|
|
181
|
+
missingEventIds++;
|
|
182
|
+
if (byPhysicalRow.has(physicalIdentity)) {
|
|
183
|
+
exactRetryDuplicates++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
byPhysicalRow.set(physicalIdentity, true);
|
|
187
|
+
} else {
|
|
188
|
+
byEvent.set(eventId, physicalIdentity);
|
|
189
|
+
}
|
|
190
|
+
parsed.push({ eventId, physicalIdentity, row });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const byChunkIndex = new Map();
|
|
194
|
+
const chunks = [];
|
|
195
|
+
for (const item of parsed) {
|
|
196
|
+
const index = Number(item.row.body_chunk_index);
|
|
197
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
198
|
+
conflicts.push({ identity: String(index), reason: "invalid_chunk_index" });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const prior = byChunkIndex.get(index);
|
|
202
|
+
if (prior) {
|
|
203
|
+
conflicts.push({
|
|
204
|
+
identity: `chunk:${index}`,
|
|
205
|
+
reason:
|
|
206
|
+
prior.physicalIdentity === item.physicalIdentity
|
|
207
|
+
? "producer_duplicate"
|
|
208
|
+
: "logical_identity_conflict",
|
|
209
|
+
firstEventId: prior.eventId,
|
|
210
|
+
duplicateEventId: item.eventId,
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
byChunkIndex.set(index, item);
|
|
215
|
+
chunks.push(item.row);
|
|
216
|
+
}
|
|
217
|
+
chunks.sort(
|
|
218
|
+
(left, right) =>
|
|
219
|
+
Number(left.body_chunk_index) - Number(right.body_chunk_index),
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
chunks,
|
|
223
|
+
diagnostics: {
|
|
224
|
+
rawChunks: rows.length,
|
|
225
|
+
storedChunks: chunks.length,
|
|
226
|
+
exactRetryDuplicates,
|
|
227
|
+
missingEventIds,
|
|
228
|
+
conflicts,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
10
233
|
/** Empty traffic, unavailable evidence, and partial queries cannot become a green check.
|
|
11
234
|
* @param {import("../../src/lib/types/index.js").ProxyTelemetryDoctorOptions} options
|
|
12
235
|
*/
|
|
@@ -141,6 +364,8 @@ export async function checkProxyTelemetry({
|
|
|
141
364
|
}
|
|
142
365
|
/** @type {Record<string, import("../../src/lib/types/index.js").ProxyTelemetryStoredRecord[]>} */
|
|
143
366
|
const history = Object.create(null);
|
|
367
|
+
/** @type {Record<string, {rawRecords: number, logicalRecords: number, exactRetryDuplicates: number, missingEventIds: number, invalidLogicalIdentities: number, conflicts: unknown[]}>} */
|
|
368
|
+
const historyDiagnostics = Object.create(null);
|
|
144
369
|
for (const kind of [
|
|
145
370
|
"request_final",
|
|
146
371
|
"body_capture_index",
|
|
@@ -165,25 +390,37 @@ export async function checkProxyTelemetry({
|
|
|
165
390
|
budget,
|
|
166
391
|
});
|
|
167
392
|
queries.push(...result.queries.map((q) => ({ ...q, kind })));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
const value = JSON.parse(row.body);
|
|
173
|
-
if (
|
|
174
|
-
!value ||
|
|
175
|
-
typeof value !== "object" ||
|
|
176
|
-
(kind === "telemetry_delivery"
|
|
177
|
-
? typeof value.id !== "string" ||
|
|
178
|
-
typeof value.at !== "string" ||
|
|
179
|
-
!Number.isFinite(Date.parse(value.at))
|
|
180
|
-
: typeof value.requestId !== "string")
|
|
181
|
-
) {
|
|
182
|
-
throw new Error("Stored telemetry metadata has an invalid schema");
|
|
183
|
-
}
|
|
184
|
-
return { ...value, recordedAtMicroseconds: Number(row._timestamp) };
|
|
185
|
-
});
|
|
393
|
+
const normalized = normalizeMetadataRows(kind, result.records);
|
|
394
|
+
history[kind] = normalized.records;
|
|
395
|
+
historyDiagnostics[kind] = normalized.diagnostics;
|
|
186
396
|
}
|
|
397
|
+
const identityConflicts = Object.values(historyDiagnostics).reduce(
|
|
398
|
+
(sum, diagnostic) =>
|
|
399
|
+
sum + diagnostic.conflicts.length + diagnostic.invalidLogicalIdentities,
|
|
400
|
+
0,
|
|
401
|
+
);
|
|
402
|
+
const missingEventIds = Object.values(historyDiagnostics).reduce(
|
|
403
|
+
(sum, diagnostic) => sum + diagnostic.missingEventIds,
|
|
404
|
+
0,
|
|
405
|
+
);
|
|
406
|
+
add(
|
|
407
|
+
"backend_row_identity",
|
|
408
|
+
identityConflicts
|
|
409
|
+
? "fail"
|
|
410
|
+
: missingEventIds
|
|
411
|
+
? "unverified"
|
|
412
|
+
: "pass",
|
|
413
|
+
{
|
|
414
|
+
scope: "selected metadata and bounded capture-settling windows",
|
|
415
|
+
exactRetryDuplicates: Object.values(historyDiagnostics).reduce(
|
|
416
|
+
(sum, diagnostic) => sum + diagnostic.exactRetryDuplicates,
|
|
417
|
+
0,
|
|
418
|
+
),
|
|
419
|
+
missingEventIds,
|
|
420
|
+
conflicts: identityConflicts,
|
|
421
|
+
byKind: historyDiagnostics,
|
|
422
|
+
},
|
|
423
|
+
);
|
|
187
424
|
/** @param {{at?: unknown}} row */
|
|
188
425
|
const failedInsideInterval = (row) => {
|
|
189
426
|
const at = Date.parse(typeof row.at === "string" ? row.at : "");
|
|
@@ -312,14 +549,23 @@ export async function checkProxyTelemetry({
|
|
|
312
549
|
);
|
|
313
550
|
const finals = history.request_final,
|
|
314
551
|
unique = new Set(finals.map((r) => r.requestId));
|
|
552
|
+
const finalDiagnostics = historyDiagnostics.request_final;
|
|
315
553
|
add(
|
|
316
554
|
"final_uniqueness",
|
|
317
555
|
!finals.length
|
|
318
556
|
? "unverified"
|
|
319
|
-
: unique.size === finals.length
|
|
557
|
+
: unique.size === finals.length &&
|
|
558
|
+
!finalDiagnostics.conflicts.length &&
|
|
559
|
+
!finalDiagnostics.invalidLogicalIdentities
|
|
320
560
|
? "pass"
|
|
321
561
|
: "fail",
|
|
322
|
-
{
|
|
562
|
+
{
|
|
563
|
+
records: finals.length,
|
|
564
|
+
rawRecords: finalDiagnostics.rawRecords,
|
|
565
|
+
exactRetryDuplicates: finalDiagnostics.exactRetryDuplicates,
|
|
566
|
+
uniqueRequestIds: unique.size,
|
|
567
|
+
producerOrConflictingDuplicates: finalDiagnostics.conflicts.length,
|
|
568
|
+
},
|
|
323
569
|
);
|
|
324
570
|
const terminalEvents = history.lifecycle.filter(
|
|
325
571
|
(row) => row.event === "request_terminal",
|
|
@@ -493,8 +739,8 @@ export async function checkProxyTelemetry({
|
|
|
493
739
|
) {
|
|
494
740
|
throw new Error("Capture verification exceeds the 8 MiB per-body bound");
|
|
495
741
|
}
|
|
496
|
-
const
|
|
497
|
-
`SELECT body_chunk_index, body_chunk_count, body FROM "${backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${index.captureId}' ORDER BY body_chunk_index ASC`,
|
|
742
|
+
const rawChunks = await query(
|
|
743
|
+
`SELECT _timestamp, proxy_event_id, body_capture_id, body_chunk_index, body_chunk_count, body FROM "${backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${index.captureId}' ORDER BY body_chunk_index ASC, proxy_event_id ASC`,
|
|
498
744
|
"logs",
|
|
499
745
|
{
|
|
500
746
|
startTime: startTime - 120e6,
|
|
@@ -502,9 +748,11 @@ export async function checkProxyTelemetry({
|
|
|
502
748
|
size: 1000,
|
|
503
749
|
},
|
|
504
750
|
);
|
|
505
|
-
if (
|
|
751
|
+
if (rawChunks.length >= 1000) {
|
|
506
752
|
throw new Error("Capture verification exceeded its chunk budget");
|
|
507
753
|
}
|
|
754
|
+
const normalizedChunks = normalizeBodyChunkRows(rawChunks);
|
|
755
|
+
const chunks = normalizedChunks.chunks;
|
|
508
756
|
const bytes = chunks.reduce(
|
|
509
757
|
(sum, r) =>
|
|
510
758
|
sum +
|
|
@@ -526,10 +774,17 @@ export async function checkProxyTelemetry({
|
|
|
526
774
|
captureId: index.captureId,
|
|
527
775
|
expectedChunks: expected,
|
|
528
776
|
storedChunks: chunks.length,
|
|
777
|
+
rawStoredChunks: rawChunks.length,
|
|
778
|
+
exactRetryDuplicates:
|
|
779
|
+
normalizedChunks.diagnostics.exactRetryDuplicates,
|
|
780
|
+
missingEventIds: normalizedChunks.diagnostics.missingEventIds,
|
|
781
|
+
identityConflicts: normalizedChunks.diagnostics.conflicts,
|
|
529
782
|
verified:
|
|
530
783
|
expected > 0 &&
|
|
531
784
|
expected === chunks.length &&
|
|
532
785
|
contiguous &&
|
|
786
|
+
normalizedChunks.diagnostics.missingEventIds === 0 &&
|
|
787
|
+
normalizedChunks.diagnostics.conflicts.length === 0 &&
|
|
533
788
|
Buffer.byteLength(raw) === index.redactedBodyBytes &&
|
|
534
789
|
actualSha256 === index.bodySha256,
|
|
535
790
|
});
|
|
@@ -601,6 +856,26 @@ export async function checkProxyTelemetry({
|
|
|
601
856
|
return [match?.[1] ?? "invalid_sample", Number(match?.[2])];
|
|
602
857
|
}),
|
|
603
858
|
);
|
|
859
|
+
const counterEntries = Object.entries(counters);
|
|
860
|
+
const invalidCounters = counterEntries.filter(
|
|
861
|
+
([, value]) => !Number.isFinite(value) || value < 0,
|
|
862
|
+
);
|
|
863
|
+
const consequentialFailures = counterEntries.filter(
|
|
864
|
+
([key, value]) =>
|
|
865
|
+
value > 0 &&
|
|
866
|
+
(/^otelcol_(exporter_enqueue_failed|receiver_(refused|failed))/.test(
|
|
867
|
+
key,
|
|
868
|
+
) ||
|
|
869
|
+
(/^otelcol_exporter_send_failed/.test(key) &&
|
|
870
|
+
!/error_permanent="false"/.test(key))),
|
|
871
|
+
);
|
|
872
|
+
const transientRetryAttempts = counterEntries
|
|
873
|
+
.filter(
|
|
874
|
+
([key]) =>
|
|
875
|
+
/^otelcol_exporter_send_failed/.test(key) &&
|
|
876
|
+
/error_permanent="false"/.test(key),
|
|
877
|
+
)
|
|
878
|
+
.reduce((sum, [, value]) => sum + value, 0);
|
|
604
879
|
add(
|
|
605
880
|
"collector_delivery",
|
|
606
881
|
["log_records", "spans", "metric_points"].every((signal) =>
|
|
@@ -610,21 +885,19 @@ export async function checkProxyTelemetry({
|
|
|
610
885
|
key.startsWith(`otelcol_exporter_sent_${signal}`),
|
|
611
886
|
),
|
|
612
887
|
)
|
|
613
|
-
?
|
|
614
|
-
([key, value]) =>
|
|
615
|
-
!Number.isFinite(value) ||
|
|
616
|
-
value < 0 ||
|
|
617
|
-
(/^otelcol_(exporter_(send_failed|enqueue_failed)|receiver_(refused|failed))/.test(
|
|
618
|
-
key,
|
|
619
|
-
) &&
|
|
620
|
-
value > 0),
|
|
621
|
-
)
|
|
888
|
+
? invalidCounters.length || consequentialFailures.length
|
|
622
889
|
? "warn"
|
|
623
890
|
: "pass"
|
|
624
891
|
: "unverified",
|
|
625
892
|
{
|
|
626
893
|
scope:
|
|
627
|
-
"collector lifetime delivery/failure counters;
|
|
894
|
+
"collector lifetime delivery/failure counters; transient retry attempts are historical pressure rather than proof of loss, while permanent, enqueue, receiver, and invalid counters remain warnings; selected-interval loss is checked against stored request and capture identities",
|
|
895
|
+
transientRetryAttempts,
|
|
896
|
+
consequentialFailures: consequentialFailures.map(([key, value]) => ({
|
|
897
|
+
key,
|
|
898
|
+
value,
|
|
899
|
+
})),
|
|
900
|
+
invalidCounters: invalidCounters.map(([key, value]) => ({ key, value })),
|
|
628
901
|
counters,
|
|
629
902
|
},
|
|
630
903
|
);
|
|
@@ -96,7 +96,7 @@ export async function queryProxyHistory({
|
|
|
96
96
|
query: {
|
|
97
97
|
// The body is small metadata, not arbitrary request/response chunks.
|
|
98
98
|
// Include a secondary key: timestamp-only paging loses equal-time events.
|
|
99
|
-
sql: `SELECT _timestamp, service_instance_id, request_id, body FROM "${stream}" WHERE proxy_record_kind='${kind}' ORDER BY _timestamp ASC, service_instance_id ASC, request_id ASC, body ASC`,
|
|
99
|
+
sql: `SELECT _timestamp, proxy_event_id, service_instance_id, request_id, body FROM "${stream}" WHERE proxy_record_kind='${kind}' ORDER BY _timestamp ASC, proxy_event_id ASC, service_instance_id ASC, request_id ASC, body ASC`,
|
|
100
100
|
start_time: start,
|
|
101
101
|
end_time: end - 1,
|
|
102
102
|
from: offset,
|