@blogic-cz/agent-tools 0.12.0 → 0.13.0
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/package.json +1 -1
- package/src/gh-tool/release.ts +10 -2
- package/src/observability-tool/trace.ts +247 -61
- package/src/observability-tool/types.ts +48 -0
package/package.json
CHANGED
package/src/gh-tool/release.ts
CHANGED
|
@@ -72,7 +72,8 @@ type LatestRelease = {
|
|
|
72
72
|
tagName: string;
|
|
73
73
|
name: string;
|
|
74
74
|
createdAt: string;
|
|
75
|
-
|
|
75
|
+
publishedAt: string | null;
|
|
76
|
+
isLatest: boolean;
|
|
76
77
|
};
|
|
77
78
|
|
|
78
79
|
type ReleaseStatusResult = {
|
|
@@ -314,7 +315,14 @@ const deleteRelease = Effect.fn("release.deleteRelease")(function* (opts: {
|
|
|
314
315
|
const releaseStatus = Effect.fn("release.releaseStatus")(function* (repo: string | null) {
|
|
315
316
|
const gh = yield* GitHubService;
|
|
316
317
|
|
|
317
|
-
const args = [
|
|
318
|
+
const args = [
|
|
319
|
+
"release",
|
|
320
|
+
"list",
|
|
321
|
+
"--json",
|
|
322
|
+
"tagName,name,createdAt,publishedAt,isLatest",
|
|
323
|
+
"--limit",
|
|
324
|
+
"1",
|
|
325
|
+
];
|
|
318
326
|
if (repo !== null) {
|
|
319
327
|
args.push("--repo", repo);
|
|
320
328
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Console, Effect } from "effect";
|
|
1
|
+
import { Console, Effect, type Option } from "effect";
|
|
2
2
|
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
3
|
|
|
4
4
|
import { formatOption, formatOutput } from "#shared";
|
|
5
|
+
import type { OutputFormat } from "#shared";
|
|
5
6
|
|
|
6
7
|
import { ObservabilityToolError } from "./errors";
|
|
7
8
|
import {
|
|
@@ -14,14 +15,31 @@ import {
|
|
|
14
15
|
} from "./shared";
|
|
15
16
|
import type {
|
|
16
17
|
FlattenedSpan,
|
|
18
|
+
ObservabilityEnvConfig,
|
|
17
19
|
OtlpAnyValue,
|
|
18
20
|
OtlpAttribute,
|
|
21
|
+
ParsedId,
|
|
22
|
+
SearchWindow,
|
|
23
|
+
SpanResolution,
|
|
24
|
+
TempoSearchResponse,
|
|
19
25
|
TempoTraceResponse,
|
|
20
26
|
TraceSummary,
|
|
21
27
|
} from "./types";
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
const SPAN_SEARCH_WINDOWS: SearchWindow[] = [
|
|
30
|
+
{ start: "now-1h", end: "now" },
|
|
31
|
+
{ start: "now-24h", end: "now" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function parseId(value: string): ParsedId | undefined {
|
|
35
|
+
const trimmed = value.trim().toLowerCase();
|
|
36
|
+
if (/^[\da-f]{32}$/.test(trimmed)) {
|
|
37
|
+
return { rawId: value, normalizedId: trimmed, kind: "trace_id" };
|
|
38
|
+
}
|
|
39
|
+
if (/^[\da-f]{16}$/.test(trimmed)) {
|
|
40
|
+
return { rawId: value, normalizedId: trimmed, kind: "span_id" };
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
function getStringAttribute(
|
|
@@ -111,6 +129,16 @@ function computeDurationMs(start?: string, end?: string): number | undefined {
|
|
|
111
129
|
return Number(endNano - startNano) / 1_000_000;
|
|
112
130
|
}
|
|
113
131
|
|
|
132
|
+
function relativeToEpoch(value: string, nowEpoch: number): number {
|
|
133
|
+
const match = /^now-(\d+)([smhd])$/.exec(value.trim());
|
|
134
|
+
if (!match) return nowEpoch;
|
|
135
|
+
|
|
136
|
+
const amount = Number(match[1]);
|
|
137
|
+
const unit = match[2];
|
|
138
|
+
const multipliers: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
139
|
+
return nowEpoch - amount * (multipliers[unit] ?? 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
function isErrorStatus(code?: string | number): boolean {
|
|
115
143
|
if (code === undefined) return false;
|
|
116
144
|
if (typeof code === "number") return code === 2;
|
|
@@ -205,72 +233,207 @@ function parseLabel(value: string | Record<string, string>): Record<string, stri
|
|
|
205
233
|
}
|
|
206
234
|
}
|
|
207
235
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
type ResolvedTrace = {
|
|
237
|
+
readonly resolution: SpanResolution;
|
|
238
|
+
readonly spans: FlattenedSpan[];
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
function searchTempoBySpanId(
|
|
242
|
+
config: ObservabilityEnvConfig,
|
|
243
|
+
spanId: string,
|
|
244
|
+
window: SearchWindow,
|
|
245
|
+
): Effect.Effect<TempoSearchResponse, ObservabilityToolError> {
|
|
246
|
+
const now = Math.floor(Date.now() / 1000);
|
|
247
|
+
const startEpoch = relativeToEpoch(window.start, now);
|
|
248
|
+
const endEpoch = relativeToEpoch(window.end, now);
|
|
249
|
+
const traceql = encodeURIComponent(`{ span:id = "${spanId}" }`);
|
|
250
|
+
const searchUrl =
|
|
251
|
+
`/api/datasources/proxy/uid/${config.tempoUid}/api/search` +
|
|
252
|
+
`?q=${traceql}&start=${startEpoch}&end=${endEpoch}&limit=5`;
|
|
253
|
+
|
|
254
|
+
return observabilityFetch<TempoSearchResponse>(config, searchUrl);
|
|
255
|
+
}
|
|
218
256
|
|
|
219
|
-
|
|
220
|
-
|
|
257
|
+
function fetchFullTrace(
|
|
258
|
+
config: ObservabilityEnvConfig,
|
|
259
|
+
traceId: string,
|
|
260
|
+
): Effect.Effect<FlattenedSpan[], ObservabilityToolError> {
|
|
261
|
+
return Effect.gen(function* () {
|
|
262
|
+
const raw = yield* observabilityFetch<TempoTraceResponse>(
|
|
263
|
+
config,
|
|
264
|
+
`/api/datasources/proxy/uid/${config.tempoUid}/api/traces/${traceId}`,
|
|
265
|
+
);
|
|
266
|
+
return flattenTrace(raw);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveTraceFromId(
|
|
271
|
+
config: ObservabilityEnvConfig,
|
|
272
|
+
parsed: ParsedId,
|
|
273
|
+
explicitWindows?: { start: string; end: string },
|
|
274
|
+
): Effect.Effect<ResolvedTrace, ObservabilityToolError> {
|
|
275
|
+
return Effect.gen(function* () {
|
|
276
|
+
if (parsed.kind === "trace_id") {
|
|
277
|
+
const spans = yield* fetchFullTrace(config, parsed.normalizedId);
|
|
278
|
+
if (spans.length === 0) {
|
|
221
279
|
return yield* new ObservabilityToolError({
|
|
222
|
-
cause: new Error(
|
|
280
|
+
cause: new Error(`Trace ${parsed.normalizedId} returned zero spans`),
|
|
223
281
|
});
|
|
224
282
|
}
|
|
283
|
+
return {
|
|
284
|
+
resolution: {
|
|
285
|
+
via: "direct_trace_id" as const,
|
|
286
|
+
resolvedTraceId: parsed.normalizedId,
|
|
287
|
+
},
|
|
288
|
+
spans,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
225
291
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`/api/datasources/proxy/uid/${config.tempoUid}/api/traces/${traceId.toLowerCase()}`,
|
|
230
|
-
);
|
|
231
|
-
const spans = flattenTrace(raw);
|
|
292
|
+
const windows = explicitWindows
|
|
293
|
+
? [{ start: explicitWindows.start, end: explicitWindows.end }]
|
|
294
|
+
: SPAN_SEARCH_WINDOWS;
|
|
232
295
|
|
|
233
|
-
|
|
296
|
+
const attemptedWindows: SearchWindow[] = [];
|
|
297
|
+
let usedWindow: SearchWindow | undefined;
|
|
298
|
+
let uniqueTraceIds: string[] = [];
|
|
299
|
+
|
|
300
|
+
for (const window of windows) {
|
|
301
|
+
attemptedWindows.push(window);
|
|
302
|
+
const searchResult = yield* searchTempoBySpanId(config, parsed.normalizedId, window);
|
|
303
|
+
const traces = searchResult.traces ?? [];
|
|
304
|
+
|
|
305
|
+
if (traces.length === 0) continue;
|
|
306
|
+
|
|
307
|
+
const candidateTraceIds = traces
|
|
308
|
+
.map((trace) => trace.traceID?.toLowerCase())
|
|
309
|
+
.filter((id): id is string => id !== undefined);
|
|
310
|
+
|
|
311
|
+
uniqueTraceIds = [...new Set(candidateTraceIds)];
|
|
312
|
+
|
|
313
|
+
if (uniqueTraceIds.length > 1) {
|
|
234
314
|
return yield* new ObservabilityToolError({
|
|
235
|
-
cause:
|
|
315
|
+
cause: {
|
|
316
|
+
message: `Ambiguous span ID ${parsed.normalizedId} — found in ${uniqueTraceIds.length} traces`,
|
|
317
|
+
code: "AMBIGUOUS_SPAN_ID",
|
|
318
|
+
retryable: true,
|
|
319
|
+
details: { candidateTraceIds: uniqueTraceIds },
|
|
320
|
+
},
|
|
236
321
|
});
|
|
237
322
|
}
|
|
238
323
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
324
|
+
usedWindow = window;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (uniqueTraceIds.length === 0 || !usedWindow) {
|
|
329
|
+
const windowDesc = attemptedWindows
|
|
330
|
+
.map((window) => `${window.start} → ${window.end}`)
|
|
331
|
+
.join(", ");
|
|
332
|
+
return yield* new ObservabilityToolError({
|
|
333
|
+
cause: new Error(
|
|
334
|
+
`No trace found containing span ${parsed.normalizedId} (searched windows: ${windowDesc})`,
|
|
335
|
+
),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const traceId = uniqueTraceIds[0];
|
|
340
|
+
const spans = yield* fetchFullTrace(config, traceId);
|
|
341
|
+
|
|
342
|
+
if (spans.length === 0) {
|
|
343
|
+
return yield* new ObservabilityToolError({
|
|
344
|
+
cause: new Error(`Trace ${traceId} returned zero spans`),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const focusSpan = spans.find((span) => span.spanId === parsed.normalizedId);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
resolution: {
|
|
352
|
+
via: "span_search" as const,
|
|
353
|
+
resolvedTraceId: traceId,
|
|
354
|
+
searchedSpanId: parsed.normalizedId,
|
|
355
|
+
focusSpan,
|
|
356
|
+
attemptedWindows,
|
|
357
|
+
usedWindow,
|
|
358
|
+
},
|
|
359
|
+
spans,
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function handleTraceGet(
|
|
365
|
+
id: string,
|
|
366
|
+
format: OutputFormat,
|
|
367
|
+
env: string,
|
|
368
|
+
profile: Option.Option<string>,
|
|
369
|
+
) {
|
|
370
|
+
const startedAt = Date.now();
|
|
371
|
+
|
|
372
|
+
return Effect.gen(function* () {
|
|
373
|
+
const parsed = parseId(id);
|
|
374
|
+
if (!parsed) {
|
|
375
|
+
return yield* new ObservabilityToolError({
|
|
376
|
+
cause: {
|
|
377
|
+
message: `Invalid ID format: expected 32-char trace ID or 16-char span ID, got ${id.length} characters`,
|
|
378
|
+
code: "INVALID_ID_FORMAT",
|
|
379
|
+
retryable: false,
|
|
248
380
|
},
|
|
249
|
-
|
|
250
|
-
|
|
381
|
+
});
|
|
382
|
+
}
|
|
251
383
|
|
|
252
|
-
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
384
|
+
const config = yield* resolveConfig(env, profile);
|
|
385
|
+
const { resolution, spans } = yield* resolveTraceFromId(config, parsed);
|
|
386
|
+
|
|
387
|
+
const result = {
|
|
388
|
+
success: true,
|
|
389
|
+
message:
|
|
390
|
+
parsed.kind === "span_id"
|
|
391
|
+
? `Found trace ${resolution.resolvedTraceId} via span ${parsed.normalizedId} with ${spans.length} span(s)`
|
|
392
|
+
: `Resolved trace ${resolution.resolvedTraceId} with ${spans.length} span(s)`,
|
|
393
|
+
data: {
|
|
394
|
+
environment: env,
|
|
395
|
+
grafanaUrl: config.url,
|
|
396
|
+
tempoDatasourceUid: config.tempoUid,
|
|
397
|
+
input: parsed,
|
|
398
|
+
resolution,
|
|
399
|
+
summary: summarizeTrace(resolution.resolvedTraceId, spans),
|
|
400
|
+
spans,
|
|
401
|
+
},
|
|
402
|
+
executionTimeMs: Date.now() - startedAt,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
yield* Console.log(formatOutput(result, format));
|
|
406
|
+
}).pipe(
|
|
407
|
+
Effect.catch((error) =>
|
|
408
|
+
Effect.gen(function* () {
|
|
409
|
+
const result = {
|
|
410
|
+
success: false,
|
|
411
|
+
message: "Failed to resolve trace from Tempo",
|
|
412
|
+
error: formatObservabilityError(error),
|
|
413
|
+
hint: "Accepts 32-char trace ID or 16-char span ID. Check format and Grafana/Tempo connectivity",
|
|
414
|
+
executionTimeMs: Date.now() - startedAt,
|
|
415
|
+
};
|
|
416
|
+
yield* Console.log(formatOutput(result, format));
|
|
417
|
+
}),
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const getCommand = Command.make(
|
|
423
|
+
"get",
|
|
424
|
+
{
|
|
425
|
+
id: Argument.string("id"),
|
|
426
|
+
format: formatOption,
|
|
427
|
+
env: envOption,
|
|
428
|
+
profile: profileOption,
|
|
267
429
|
},
|
|
268
|
-
|
|
430
|
+
({ id, format, env, profile }) => handleTraceGet(id, format, env, profile),
|
|
431
|
+
).pipe(Command.withDescription("Resolve a trace by trace ID or span ID via Grafana/Tempo"));
|
|
269
432
|
|
|
270
433
|
const logsCommand = Command.make(
|
|
271
434
|
"logs",
|
|
272
435
|
{
|
|
273
|
-
|
|
436
|
+
id: Argument.string("id"),
|
|
274
437
|
format: formatOption,
|
|
275
438
|
env: envOption,
|
|
276
439
|
profile: profileOption,
|
|
@@ -287,19 +450,26 @@ const logsCommand = Command.make(
|
|
|
287
450
|
Flag.withDefault("now"),
|
|
288
451
|
),
|
|
289
452
|
},
|
|
290
|
-
({
|
|
453
|
+
({ id, format, env, profile, limit, start, end }) => {
|
|
291
454
|
const startedAt = Date.now();
|
|
292
455
|
|
|
293
456
|
return Effect.gen(function* () {
|
|
294
|
-
|
|
457
|
+
const parsed = parseId(id);
|
|
458
|
+
if (!parsed) {
|
|
295
459
|
return yield* new ObservabilityToolError({
|
|
296
|
-
cause:
|
|
460
|
+
cause: {
|
|
461
|
+
message: `Invalid ID format: expected 32-char trace ID or 16-char span ID, got ${id.length} characters`,
|
|
462
|
+
code: "INVALID_ID_FORMAT",
|
|
463
|
+
retryable: false,
|
|
464
|
+
},
|
|
297
465
|
});
|
|
298
466
|
}
|
|
299
467
|
|
|
300
468
|
const config = yield* resolveConfig(env, profile);
|
|
301
|
-
const
|
|
302
|
-
const
|
|
469
|
+
const { resolution } = yield* resolveTraceFromId(config, parsed, { start, end });
|
|
470
|
+
const resolvedTraceId = resolution.resolvedTraceId;
|
|
471
|
+
|
|
472
|
+
const logql = `{job=~".+"} |= "${resolvedTraceId}"`;
|
|
303
473
|
const response = yield* observabilityDsQuery(config, config.lokiUid, "loki", logql, {
|
|
304
474
|
from: start,
|
|
305
475
|
to: end,
|
|
@@ -343,11 +513,16 @@ const logsCommand = Command.make(
|
|
|
343
513
|
|
|
344
514
|
const result = {
|
|
345
515
|
success: true,
|
|
346
|
-
message:
|
|
516
|
+
message:
|
|
517
|
+
parsed.kind === "span_id"
|
|
518
|
+
? `Found ${logs.length} log line(s) for trace ${resolvedTraceId} (resolved from span ${parsed.normalizedId})`
|
|
519
|
+
: `Found ${logs.length} log line(s) mentioning trace ${resolvedTraceId}`,
|
|
347
520
|
data: {
|
|
348
521
|
environment: env,
|
|
349
522
|
grafanaUrl: config.url,
|
|
350
523
|
lokiDatasourceUid: config.lokiUid,
|
|
524
|
+
input: parsed,
|
|
525
|
+
resolution,
|
|
351
526
|
query: logql,
|
|
352
527
|
logCount: logs.length,
|
|
353
528
|
logs: logs.toSorted((left, right) => right.timestamp.localeCompare(left.timestamp)),
|
|
@@ -363,7 +538,7 @@ const logsCommand = Command.make(
|
|
|
363
538
|
success: false,
|
|
364
539
|
message: "Failed to execute trace log lookup",
|
|
365
540
|
error: formatObservabilityError(error),
|
|
366
|
-
hint: "
|
|
541
|
+
hint: "Accepts 32-char trace ID or 16-char span ID. Check format and Grafana/Loki connectivity",
|
|
367
542
|
executionTimeMs: Date.now() - startedAt,
|
|
368
543
|
};
|
|
369
544
|
yield* Console.log(formatOutput(result, format));
|
|
@@ -371,9 +546,20 @@ const logsCommand = Command.make(
|
|
|
371
546
|
),
|
|
372
547
|
);
|
|
373
548
|
},
|
|
374
|
-
).pipe(Command.withDescription("Find Loki logs mentioning a trace ID"));
|
|
549
|
+
).pipe(Command.withDescription("Find Loki logs mentioning a trace (accepts trace ID or span ID)"));
|
|
550
|
+
|
|
551
|
+
const findCommand = Command.make(
|
|
552
|
+
"find",
|
|
553
|
+
{
|
|
554
|
+
id: Argument.string("id"),
|
|
555
|
+
format: formatOption,
|
|
556
|
+
env: envOption,
|
|
557
|
+
profile: profileOption,
|
|
558
|
+
},
|
|
559
|
+
({ id, format, env, profile }) => handleTraceGet(id, format, env, profile),
|
|
560
|
+
).pipe(Command.withDescription("Alias for 'trace get' — resolve a trace by trace ID or span ID"));
|
|
375
561
|
|
|
376
562
|
export const traceCommand = Command.make("trace", {}).pipe(
|
|
377
563
|
Command.withDescription("Tempo trace operations"),
|
|
378
|
-
Command.withSubcommands([getCommand, logsCommand]),
|
|
564
|
+
Command.withSubcommands([getCommand, logsCommand, findCommand]),
|
|
379
565
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
1
3
|
export type ObservabilityEnvConfig = {
|
|
2
4
|
url: string;
|
|
3
5
|
token?: string;
|
|
@@ -89,6 +91,52 @@ export type TraceSummary = {
|
|
|
89
91
|
readonly endedAtUnixNano?: string;
|
|
90
92
|
};
|
|
91
93
|
|
|
94
|
+
export type TempoSearchResponse = {
|
|
95
|
+
readonly traces?: ReadonlyArray<{
|
|
96
|
+
readonly traceID?: string;
|
|
97
|
+
readonly rootServiceName?: string;
|
|
98
|
+
readonly rootTraceName?: string;
|
|
99
|
+
readonly startTimeUnixNano?: string;
|
|
100
|
+
readonly durationMs?: number;
|
|
101
|
+
readonly spanSets?: ReadonlyArray<{
|
|
102
|
+
readonly spans?: ReadonlyArray<{
|
|
103
|
+
readonly spanID?: string;
|
|
104
|
+
readonly startTimeUnixNano?: string;
|
|
105
|
+
readonly durationNanos?: string;
|
|
106
|
+
readonly attributes?: ReadonlyArray<OtlpAttribute>;
|
|
107
|
+
}>;
|
|
108
|
+
readonly matched?: number;
|
|
109
|
+
}>;
|
|
110
|
+
}>;
|
|
111
|
+
readonly metrics?: Record<string, unknown>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const IdKind = Schema.Literals(["trace_id", "span_id"]);
|
|
115
|
+
export type IdKind = typeof IdKind.Type;
|
|
116
|
+
|
|
117
|
+
export const ResolutionVia = Schema.Literals(["direct_trace_id", "span_search"]);
|
|
118
|
+
export type ResolutionVia = typeof ResolutionVia.Type;
|
|
119
|
+
|
|
120
|
+
export type ParsedId = {
|
|
121
|
+
readonly rawId: string;
|
|
122
|
+
readonly normalizedId: string;
|
|
123
|
+
readonly kind: IdKind;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type SearchWindow = {
|
|
127
|
+
readonly start: string;
|
|
128
|
+
readonly end: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type SpanResolution = {
|
|
132
|
+
readonly via: ResolutionVia;
|
|
133
|
+
readonly resolvedTraceId: string;
|
|
134
|
+
readonly searchedSpanId?: string;
|
|
135
|
+
readonly focusSpan?: FlattenedSpan;
|
|
136
|
+
readonly attemptedWindows?: ReadonlyArray<SearchWindow>;
|
|
137
|
+
readonly usedWindow?: SearchWindow;
|
|
138
|
+
};
|
|
139
|
+
|
|
92
140
|
export type DsQueryOpts = {
|
|
93
141
|
instant?: boolean;
|
|
94
142
|
from?: string;
|