@sincpro/mobile 0.2.0 → 0.4.1

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 (95) hide show
  1. package/README.md +1063 -47
  2. package/dist/entrypoints/db/migrations.d.ts +3 -1
  3. package/dist/entrypoints/db/migrations.js +65 -0
  4. package/dist/entrypoints/queue/QueueProcessor.js +1 -25
  5. package/dist/entrypoints/queue/subscriber_filter.d.ts +9 -0
  6. package/dist/entrypoints/queue/subscriber_filter.js +25 -0
  7. package/dist/entrypoints/ui/AppShell.d.ts +55 -1
  8. package/dist/entrypoints/ui/AppShell.js +50 -5
  9. package/dist/entrypoints/ui/theme.d.ts +8 -7
  10. package/dist/entrypoints/ui/theme.js +8 -7
  11. package/dist/framework/base_module.d.ts +1 -0
  12. package/dist/framework/base_module.js +6 -1
  13. package/dist/framework/createApp.d.ts +6 -0
  14. package/dist/framework/createApp.js +5 -1
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.js +3 -0
  17. package/dist/infrastructure/context_manager/context.d.ts +16 -0
  18. package/dist/infrastructure/context_manager/context.js +37 -0
  19. package/dist/infrastructure/context_manager/context_api.d.ts +28 -0
  20. package/dist/infrastructure/context_manager/context_api.js +55 -0
  21. package/dist/infrastructure/context_manager/context_key.d.ts +15 -0
  22. package/dist/infrastructure/context_manager/context_key.js +3 -0
  23. package/dist/infrastructure/context_manager/context_manager_facade.d.ts +107 -0
  24. package/dist/infrastructure/context_manager/context_manager_facade.js +113 -0
  25. package/dist/infrastructure/context_manager/decorators/propagate_context.d.ts +24 -0
  26. package/dist/infrastructure/context_manager/decorators/propagate_context.js +52 -0
  27. package/dist/infrastructure/context_manager/decorators/with_context.d.ts +34 -0
  28. package/dist/infrastructure/context_manager/decorators/with_context.js +44 -0
  29. package/dist/infrastructure/context_manager/index.d.ts +11 -0
  30. package/dist/infrastructure/context_manager/index.js +12 -0
  31. package/dist/infrastructure/context_manager/managed_context.d.ts +87 -0
  32. package/dist/infrastructure/context_manager/managed_context.js +144 -0
  33. package/dist/infrastructure/context_manager/stack_context_manager.d.ts +39 -0
  34. package/dist/infrastructure/context_manager/stack_context_manager.js +56 -0
  35. package/dist/infrastructure/database/connector.js +10 -1
  36. package/dist/infrastructure/interceptor/index.d.ts +4 -0
  37. package/dist/infrastructure/interceptor/index.js +2 -0
  38. package/dist/infrastructure/interceptor/intercept.d.ts +57 -0
  39. package/dist/infrastructure/interceptor/intercept.js +112 -0
  40. package/dist/infrastructure/interceptor/logging_hooks.d.ts +22 -0
  41. package/dist/infrastructure/interceptor/logging_hooks.js +27 -0
  42. package/dist/infrastructure/logger.d.ts +11 -16
  43. package/dist/infrastructure/logger.js +61 -18
  44. package/dist/infrastructure/telemetry/buffer_registry.d.ts +15 -0
  45. package/dist/infrastructure/telemetry/buffer_registry.js +26 -0
  46. package/dist/infrastructure/telemetry/connectivity_state.d.ts +20 -0
  47. package/dist/infrastructure/telemetry/connectivity_state.js +28 -0
  48. package/dist/infrastructure/telemetry/context_keys.d.ts +7 -0
  49. package/dist/infrastructure/telemetry/context_keys.js +7 -0
  50. package/dist/infrastructure/telemetry/fetch_with_timeout.d.ts +8 -0
  51. package/dist/infrastructure/telemetry/fetch_with_timeout.js +17 -0
  52. package/dist/infrastructure/telemetry/flush_telemetry.d.ts +40 -0
  53. package/dist/infrastructure/telemetry/flush_telemetry.js +72 -0
  54. package/dist/infrastructure/telemetry/index.d.ts +17 -0
  55. package/dist/infrastructure/telemetry/index.js +17 -0
  56. package/dist/infrastructure/telemetry/init_telemetry.d.ts +21 -0
  57. package/dist/infrastructure/telemetry/init_telemetry.js +85 -0
  58. package/dist/infrastructure/telemetry/logging/flush_job.d.ts +44 -0
  59. package/dist/infrastructure/telemetry/logging/flush_job.js +69 -0
  60. package/dist/infrastructure/telemetry/logging/log_queue_repository.d.ts +76 -0
  61. package/dist/infrastructure/telemetry/logging/log_queue_repository.js +147 -0
  62. package/dist/infrastructure/telemetry/logging/loki_client.d.ts +32 -0
  63. package/dist/infrastructure/telemetry/logging/loki_client.js +118 -0
  64. package/dist/infrastructure/telemetry/logging/loki_registry.d.ts +12 -0
  65. package/dist/infrastructure/telemetry/logging/loki_registry.js +17 -0
  66. package/dist/infrastructure/telemetry/telemetry_flush_worker.d.ts +35 -0
  67. package/dist/infrastructure/telemetry/telemetry_flush_worker.js +65 -0
  68. package/dist/infrastructure/telemetry/telemetry_signal.d.ts +18 -0
  69. package/dist/infrastructure/telemetry/telemetry_signal.js +33 -0
  70. package/dist/infrastructure/telemetry/tracing/active_span.d.ts +22 -0
  71. package/dist/infrastructure/telemetry/tracing/active_span.js +40 -0
  72. package/dist/infrastructure/telemetry/tracing/otlp_client.d.ts +13 -0
  73. package/dist/infrastructure/telemetry/tracing/otlp_client.js +34 -0
  74. package/dist/infrastructure/telemetry/tracing/otlp_serializer.d.ts +67 -0
  75. package/dist/infrastructure/telemetry/tracing/otlp_serializer.js +101 -0
  76. package/dist/infrastructure/telemetry/tracing/span_flush_job.d.ts +25 -0
  77. package/dist/infrastructure/telemetry/tracing/span_flush_job.js +42 -0
  78. package/dist/infrastructure/telemetry/tracing/span_queue_repository.d.ts +82 -0
  79. package/dist/infrastructure/telemetry/tracing/span_queue_repository.js +134 -0
  80. package/dist/infrastructure/telemetry/tracing/span_sampler.d.ts +40 -0
  81. package/dist/infrastructure/telemetry/tracing/span_sampler.js +75 -0
  82. package/dist/infrastructure/telemetry/tracing/span_serializer.d.ts +48 -0
  83. package/dist/infrastructure/telemetry/tracing/span_serializer.js +36 -0
  84. package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.d.ts +26 -0
  85. package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.js +47 -0
  86. package/dist/infrastructure/telemetry/tracing/tracer.d.ts +22 -0
  87. package/dist/infrastructure/telemetry/tracing/tracer.js +45 -0
  88. package/dist/infrastructure/telemetry/tracing/tracing_hooks.d.ts +10 -0
  89. package/dist/infrastructure/telemetry/tracing/tracing_hooks.js +59 -0
  90. package/dist/infrastructure/telemetry/tracing_facade.d.ts +50 -0
  91. package/dist/infrastructure/telemetry/tracing_facade.js +139 -0
  92. package/dist/infrastructure/telemetry/types.d.ts +120 -0
  93. package/dist/infrastructure/telemetry/types.js +1 -0
  94. package/dist/ui/components/molecules/TimeZoneSelectorModal.js +7 -1
  95. package/package.json +17 -11
@@ -0,0 +1,82 @@
1
+ import type { DBCursor } from "../../../infrastructure/database";
2
+ import type { BufferStats } from "../types";
3
+ import type { SpanSampler } from "./span_sampler";
4
+ export interface SpanRow {
5
+ id: number;
6
+ trace_id: string;
7
+ span_id: string;
8
+ parent_span_id: string | null;
9
+ name: string;
10
+ kind: number;
11
+ start_time_unixnano: string;
12
+ end_time_unixnano: string;
13
+ attributes: string;
14
+ status_code: number;
15
+ status_message: string;
16
+ resource_attrs: string;
17
+ /** JSON array of {name, timeUnixNano, attributes} — from span.recordException() etc. */
18
+ events: string;
19
+ /** JSON array of {traceId, spanId, attributes} — cross-service links. */
20
+ links: string;
21
+ created_at: string;
22
+ }
23
+ export type SpanInput = Omit<SpanRow, "id" | "created_at">;
24
+ /** Max rows kept in spans_queue before eviction kicks in. */
25
+ export declare const MAX_SPANS_QUEUE_SIZE = 5000;
26
+ /** Max approximate payload bytes kept in spans_queue (~6 MB). */
27
+ export declare const MAX_SPANS_QUEUE_BYTES = 6000000;
28
+ /** Run the (scanning) eviction check once every N enqueues — see class doc. */
29
+ export declare const EVICT_CHECK_EVERY = 32;
30
+ /** Fraction of either budget above which head sampling activates. */
31
+ export declare const PRESSURE_FRACTION = 0.8;
32
+ /**
33
+ * Offline-first span buffer with **trace-coherent** eviction.
34
+ *
35
+ * Spans are bounded by BOTH a row count and a payload-byte budget. When either
36
+ * is exceeded the OLDEST WHOLE TRACE is dropped — never individual spans —
37
+ * so survivors are always complete, usable traces. Dropping spans by row (FIFO)
38
+ * would evict root/parent spans first and leave orphaned children that render
39
+ * as broken traces downstream.
40
+ *
41
+ * The budget check scans the table (COUNT + SUM of lengths), so it runs
42
+ * **amortized** — once every `evictEvery` enqueues — to keep the hot path cheap
43
+ * under offline bursts. The bound is therefore soft: the queue may transiently
44
+ * exceed the cap by up to `evictEvery` rows before the next trim.
45
+ *
46
+ * When a {@link SpanSampler} is supplied, head sampling kicks in once the buffer
47
+ * passes {@link PRESSURE_FRACTION} of either budget: whole traces are dropped at
48
+ * ingest (coherently) so the buffer stops thrashing instead of inserting spans
49
+ * it would immediately evict. Pressure is read from the last amortized
50
+ * measurement — no extra scan on the hot path.
51
+ *
52
+ * Telemetry is best-effort: under sustained offline pressure data IS dropped,
53
+ * but coherently and visibly (see {@link stats}).
54
+ */
55
+ export declare class SpanQueueRepository {
56
+ private readonly db;
57
+ private readonly maxRows;
58
+ private readonly maxBytes;
59
+ private readonly evictEvery;
60
+ private readonly sampler;
61
+ private ops;
62
+ private dropped;
63
+ private sampledOut;
64
+ private lastRows;
65
+ private lastBytes;
66
+ private measuredOnce;
67
+ constructor(db: typeof DBCursor, maxRows?: number, maxBytes?: number, evictEvery?: number, sampler?: SpanSampler | null);
68
+ enqueue(span: SpanInput): Promise<void>;
69
+ /** Cheap pressure check from the last amortized measurement (no scan). */
70
+ private underPressure;
71
+ /**
72
+ * Drops the oldest whole traces until both row and byte budgets are met.
73
+ * Terminates: each pass either returns (under budget / empty) or removes ≥1
74
+ * row, and the row count strictly decreases.
75
+ */
76
+ private evict;
77
+ private measure;
78
+ stats(): Promise<BufferStats>;
79
+ findPending(limit?: number): Promise<SpanRow[]>;
80
+ removeMany(ids: number[]): Promise<void>;
81
+ pruneExpired(days?: number): Promise<void>;
82
+ }
@@ -0,0 +1,134 @@
1
+ /** Max rows kept in spans_queue before eviction kicks in. */
2
+ export const MAX_SPANS_QUEUE_SIZE = 5_000;
3
+ /** Max approximate payload bytes kept in spans_queue (~6 MB). */
4
+ export const MAX_SPANS_QUEUE_BYTES = 6_000_000;
5
+ /** Run the (scanning) eviction check once every N enqueues — see class doc. */
6
+ export const EVICT_CHECK_EVERY = 32;
7
+ /** Fraction of either budget above which head sampling activates. */
8
+ export const PRESSURE_FRACTION = 0.8;
9
+ /**
10
+ * Offline-first span buffer with **trace-coherent** eviction.
11
+ *
12
+ * Spans are bounded by BOTH a row count and a payload-byte budget. When either
13
+ * is exceeded the OLDEST WHOLE TRACE is dropped — never individual spans —
14
+ * so survivors are always complete, usable traces. Dropping spans by row (FIFO)
15
+ * would evict root/parent spans first and leave orphaned children that render
16
+ * as broken traces downstream.
17
+ *
18
+ * The budget check scans the table (COUNT + SUM of lengths), so it runs
19
+ * **amortized** — once every `evictEvery` enqueues — to keep the hot path cheap
20
+ * under offline bursts. The bound is therefore soft: the queue may transiently
21
+ * exceed the cap by up to `evictEvery` rows before the next trim.
22
+ *
23
+ * When a {@link SpanSampler} is supplied, head sampling kicks in once the buffer
24
+ * passes {@link PRESSURE_FRACTION} of either budget: whole traces are dropped at
25
+ * ingest (coherently) so the buffer stops thrashing instead of inserting spans
26
+ * it would immediately evict. Pressure is read from the last amortized
27
+ * measurement — no extra scan on the hot path.
28
+ *
29
+ * Telemetry is best-effort: under sustained offline pressure data IS dropped,
30
+ * but coherently and visibly (see {@link stats}).
31
+ */
32
+ export class SpanQueueRepository {
33
+ db;
34
+ maxRows;
35
+ maxBytes;
36
+ evictEvery;
37
+ sampler;
38
+ ops = 0;
39
+ dropped = 0;
40
+ sampledOut = 0;
41
+ // Last measured size — drives the cheap pressure check on the hot path.
42
+ lastRows = 0;
43
+ lastBytes = 0;
44
+ // Whether the buffer size has been measured at least once (cold-start guard).
45
+ measuredOnce = false;
46
+ constructor(db, maxRows = MAX_SPANS_QUEUE_SIZE, maxBytes = MAX_SPANS_QUEUE_BYTES, evictEvery = EVICT_CHECK_EVERY, sampler = null) {
47
+ this.db = db;
48
+ this.maxRows = maxRows;
49
+ this.maxBytes = maxBytes;
50
+ this.evictEvery = Math.max(1, evictEvery);
51
+ this.sampler = sampler;
52
+ }
53
+ async enqueue(span) {
54
+ // Head sampling — drop whole traces at ingest while under pressure, so we
55
+ // don't insert spans only to evict them moments later. Coherent per trace.
56
+ if (this.sampler) {
57
+ // Cold-start guard: measure once before the first sampling decision so a
58
+ // buffer that booted already-full (SQLite persists) is seen as pressured.
59
+ if (!this.measuredOnce)
60
+ await this.measure();
61
+ if (!this.sampler.shouldKeep(span.trace_id, this.underPressure())) {
62
+ this.sampledOut += 1;
63
+ return;
64
+ }
65
+ }
66
+ await this.db.mutateDatabase(`INSERT INTO spans_queue
67
+ (trace_id, span_id, parent_span_id, name, kind,
68
+ start_time_unixnano, end_time_unixnano,
69
+ attributes, status_code, status_message, resource_attrs,
70
+ events, links)
71
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, span.trace_id, span.span_id, span.parent_span_id, span.name, span.kind, span.start_time_unixnano, span.end_time_unixnano, span.attributes, span.status_code, span.status_message, span.resource_attrs, span.events, span.links);
72
+ if (++this.ops >= this.evictEvery) {
73
+ this.ops = 0;
74
+ await this.evict();
75
+ }
76
+ }
77
+ /** Cheap pressure check from the last amortized measurement (no scan). */
78
+ underPressure() {
79
+ return (this.lastRows >= this.maxRows * PRESSURE_FRACTION ||
80
+ this.lastBytes >= this.maxBytes * PRESSURE_FRACTION);
81
+ }
82
+ /**
83
+ * Drops the oldest whole traces until both row and byte budgets are met.
84
+ * Terminates: each pass either returns (under budget / empty) or removes ≥1
85
+ * row, and the row count strictly decreases.
86
+ */
87
+ async evict() {
88
+ for (;;) {
89
+ const { rows, approxBytes } = await this.measure();
90
+ if (rows <= this.maxRows && approxBytes <= this.maxBytes)
91
+ return;
92
+ const res = await this.db.mutateDatabase(`DELETE FROM spans_queue WHERE trace_id = (
93
+ SELECT trace_id FROM spans_queue GROUP BY trace_id ORDER BY MIN(id) ASC LIMIT 1
94
+ )`);
95
+ const removed = res?.changes ?? 0;
96
+ if (removed === 0)
97
+ return; // queue empty — nothing left to evict
98
+ this.dropped += removed;
99
+ }
100
+ }
101
+ async measure() {
102
+ const row = await this.db.getFirstAsync(`SELECT COUNT(*) AS rows,
103
+ COALESCE(SUM(
104
+ length(trace_id) + length(span_id) + length(COALESCE(parent_span_id, ''))
105
+ + length(name) + length(start_time_unixnano) + length(end_time_unixnano)
106
+ + length(attributes) + length(status_message) + length(resource_attrs)
107
+ + length(events) + length(links)
108
+ ), 0) AS bytes
109
+ FROM spans_queue`);
110
+ const rows = row?.rows ?? 0;
111
+ const approxBytes = row?.bytes ?? 0;
112
+ // Cache for the hot-path pressure check.
113
+ this.lastRows = rows;
114
+ this.lastBytes = approxBytes;
115
+ this.measuredOnce = true;
116
+ return { rows, approxBytes };
117
+ }
118
+ async stats() {
119
+ const { rows, approxBytes } = await this.measure();
120
+ return { rows, approxBytes, dropped: this.dropped, sampled: this.sampledOut };
121
+ }
122
+ async findPending(limit = 100) {
123
+ return this.db.getAllAsync(`SELECT * FROM spans_queue ORDER BY id ASC LIMIT ?`, limit);
124
+ }
125
+ async removeMany(ids) {
126
+ if (ids.length === 0)
127
+ return;
128
+ const placeholders = ids.map(() => "?").join(", ");
129
+ await this.db.mutateDatabase(`DELETE FROM spans_queue WHERE id IN (${placeholders})`, ...ids);
130
+ }
131
+ async pruneExpired(days = 7) {
132
+ await this.db.mutateDatabase(`DELETE FROM spans_queue WHERE created_at < datetime('now', ?)`, `-${days} days`);
133
+ }
134
+ }
@@ -0,0 +1,40 @@
1
+ /** Keep 1 of every N traces while under pressure (default). */
2
+ export declare const DEFAULT_KEEP_ONE_IN_N = 4;
3
+ /** Bounded size of the per-trace decision cache. */
4
+ export declare const DEFAULT_DECISION_CACHE_SIZE = 2000;
5
+ /**
6
+ * Head sampler for spans, applied at ingest under sustained buffer pressure.
7
+ *
8
+ * The decision is **per trace and coherent**: every span sharing a `traceId`
9
+ * gets the same keep/drop verdict, even if buffer pressure changes between the
10
+ * arrival of one span and the next. This is what prevents partial/orphaned
11
+ * traces — the failure mode that makes a trace backend show broken graphs.
12
+ *
13
+ * Coherence is achieved two ways:
14
+ * - the keep verdict for a new trace is a deterministic hash of its id
15
+ * (no RNG), so the same id always hashes the same bucket; and
16
+ * - the first verdict for a trace is cached, so a mid-trace pressure flip can
17
+ * never change an already-made decision.
18
+ *
19
+ * When NOT under pressure every trace is kept. The cache is bounded (oldest
20
+ * decision evicted first). The cache is **LRU**: reading a trace's verdict
21
+ * refreshes it, so a trace still actively receiving spans keeps its decision
22
+ * resident and cannot be split by eviction. Only a trace idle long enough to
23
+ * fall out of the cache *and* hitting a pressure change can be re-decided —
24
+ * acceptable for best-effort telemetry.
25
+ */
26
+ export declare class SpanSampler {
27
+ private readonly keepOneInN;
28
+ private readonly cacheSize;
29
+ private readonly decisions;
30
+ constructor(keepOneInN?: number, cacheSize?: number);
31
+ /**
32
+ * Returns whether the span belonging to `traceId` should be buffered.
33
+ * @param underPressure - true when the buffer is near its budget; only then
34
+ * does sampling drop anything.
35
+ */
36
+ shouldKeep(traceId: string, underPressure: boolean): boolean;
37
+ /** Current number of cached trace decisions (for observability / tests). */
38
+ size(): number;
39
+ private remember;
40
+ }
@@ -0,0 +1,75 @@
1
+ /** Keep 1 of every N traces while under pressure (default). */
2
+ export const DEFAULT_KEEP_ONE_IN_N = 4;
3
+ /** Bounded size of the per-trace decision cache. */
4
+ export const DEFAULT_DECISION_CACHE_SIZE = 2_000;
5
+ /**
6
+ * Head sampler for spans, applied at ingest under sustained buffer pressure.
7
+ *
8
+ * The decision is **per trace and coherent**: every span sharing a `traceId`
9
+ * gets the same keep/drop verdict, even if buffer pressure changes between the
10
+ * arrival of one span and the next. This is what prevents partial/orphaned
11
+ * traces — the failure mode that makes a trace backend show broken graphs.
12
+ *
13
+ * Coherence is achieved two ways:
14
+ * - the keep verdict for a new trace is a deterministic hash of its id
15
+ * (no RNG), so the same id always hashes the same bucket; and
16
+ * - the first verdict for a trace is cached, so a mid-trace pressure flip can
17
+ * never change an already-made decision.
18
+ *
19
+ * When NOT under pressure every trace is kept. The cache is bounded (oldest
20
+ * decision evicted first). The cache is **LRU**: reading a trace's verdict
21
+ * refreshes it, so a trace still actively receiving spans keeps its decision
22
+ * resident and cannot be split by eviction. Only a trace idle long enough to
23
+ * fall out of the cache *and* hitting a pressure change can be re-decided —
24
+ * acceptable for best-effort telemetry.
25
+ */
26
+ export class SpanSampler {
27
+ keepOneInN;
28
+ cacheSize;
29
+ // insertion-ordered Map used as an LRU: most-recently-used key sits last.
30
+ decisions = new Map();
31
+ constructor(keepOneInN = DEFAULT_KEEP_ONE_IN_N, cacheSize = DEFAULT_DECISION_CACHE_SIZE) {
32
+ this.keepOneInN = Math.max(1, keepOneInN);
33
+ this.cacheSize = Math.max(1, cacheSize);
34
+ }
35
+ /**
36
+ * Returns whether the span belonging to `traceId` should be buffered.
37
+ * @param underPressure - true when the buffer is near its budget; only then
38
+ * does sampling drop anything.
39
+ */
40
+ shouldKeep(traceId, underPressure) {
41
+ const cached = this.decisions.get(traceId);
42
+ if (cached !== undefined) {
43
+ // Touch — move to MRU position so active traces are never evicted.
44
+ this.decisions.delete(traceId);
45
+ this.decisions.set(traceId, cached);
46
+ return cached;
47
+ }
48
+ // keepOneInN === 1 means "keep all" even under pressure.
49
+ const keep = !underPressure || this.keepOneInN === 1 || hash(traceId) % this.keepOneInN === 0;
50
+ this.remember(traceId, keep);
51
+ return keep;
52
+ }
53
+ /** Current number of cached trace decisions (for observability / tests). */
54
+ size() {
55
+ return this.decisions.size;
56
+ }
57
+ remember(traceId, keep) {
58
+ if (this.decisions.size >= this.cacheSize) {
59
+ // Evict the least-recently-used decision (first key in insertion order).
60
+ const lru = this.decisions.keys().next().value;
61
+ if (lru !== undefined)
62
+ this.decisions.delete(lru);
63
+ }
64
+ this.decisions.set(traceId, keep);
65
+ }
66
+ }
67
+ /** FNV-1a 32-bit — deterministic, fast, no allocations. */
68
+ function hash(s) {
69
+ let h = 0x811c9dc5;
70
+ for (let i = 0; i < s.length; i++) {
71
+ h ^= s.charCodeAt(i);
72
+ h = Math.imul(h, 0x01000193);
73
+ }
74
+ return h >>> 0; // unsigned
75
+ }
@@ -0,0 +1,48 @@
1
+ import type { SpanInput } from "./span_queue_repository";
2
+ /**
3
+ * OTel HrTime: [seconds: number, nanoseconds: number].
4
+ * BigInt required — seconds alone can be ~1.7e9, multiplied by 1e9 = 1.7e18,
5
+ * which exceeds Number.MAX_SAFE_INTEGER (~9e15).
6
+ */
7
+ export declare function hrTimeToNanoString(hrTime: [number, number]): string;
8
+ /**
9
+ * Minimal shape of OTel ReadableSpan needed for serialization.
10
+ * Using a local interface instead of importing from sdk-trace-base so this
11
+ * module stays importable in the Node test runner (OTel packages are CJS).
12
+ */
13
+ export interface SerializableSpanEvent {
14
+ name: string;
15
+ time: [number, number];
16
+ attributes?: Record<string, unknown>;
17
+ }
18
+ export interface SerializableSpanLink {
19
+ context: {
20
+ traceId: string;
21
+ spanId: string;
22
+ };
23
+ attributes?: Record<string, unknown>;
24
+ }
25
+ export interface SerializableSpan {
26
+ name: string;
27
+ kind: number;
28
+ spanContext(): {
29
+ traceId: string;
30
+ spanId: string;
31
+ };
32
+ parentSpanContext?: {
33
+ spanId: string;
34
+ } | undefined;
35
+ startTime: [number, number];
36
+ endTime: [number, number];
37
+ attributes: Record<string, unknown>;
38
+ status: {
39
+ code: number;
40
+ message?: string;
41
+ };
42
+ resource?: {
43
+ attributes: Record<string, unknown>;
44
+ };
45
+ events?: SerializableSpanEvent[];
46
+ links?: SerializableSpanLink[];
47
+ }
48
+ export declare function serializeSpan(span: SerializableSpan): SpanInput;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OTel HrTime: [seconds: number, nanoseconds: number].
3
+ * BigInt required — seconds alone can be ~1.7e9, multiplied by 1e9 = 1.7e18,
4
+ * which exceeds Number.MAX_SAFE_INTEGER (~9e15).
5
+ */
6
+ export function hrTimeToNanoString(hrTime) {
7
+ return String(BigInt(hrTime[0]) * 1000000000n + BigInt(hrTime[1]));
8
+ }
9
+ export function serializeSpan(span) {
10
+ const ctx = span.spanContext();
11
+ const events = (span.events ?? []).map((e) => ({
12
+ name: e.name,
13
+ timeUnixNano: hrTimeToNanoString(e.time),
14
+ attributes: e.attributes ?? {},
15
+ }));
16
+ const links = (span.links ?? []).map((l) => ({
17
+ traceId: l.context.traceId,
18
+ spanId: l.context.spanId,
19
+ attributes: l.attributes ?? {},
20
+ }));
21
+ return {
22
+ trace_id: ctx.traceId,
23
+ span_id: ctx.spanId,
24
+ parent_span_id: span.parentSpanContext?.spanId ?? null,
25
+ name: span.name,
26
+ kind: span.kind,
27
+ start_time_unixnano: hrTimeToNanoString(span.startTime),
28
+ end_time_unixnano: hrTimeToNanoString(span.endTime),
29
+ attributes: JSON.stringify(span.attributes ?? {}),
30
+ status_code: span.status.code,
31
+ status_message: span.status.message ?? "",
32
+ resource_attrs: JSON.stringify(span.resource?.attributes ?? {}),
33
+ events: JSON.stringify(events),
34
+ links: JSON.stringify(links),
35
+ };
36
+ }
@@ -0,0 +1,26 @@
1
+ import type { ExportResult } from "@opentelemetry/core";
2
+ import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
3
+ import type { SpanQueueRepository } from "./span_queue_repository";
4
+ /**
5
+ * OTel SpanExporter that persists spans to SQLite instead of sending them
6
+ * directly over the network. The flush cron drains the queue when online.
7
+ *
8
+ * Pass `enabled: false` to register the provider without storing anything —
9
+ * useful when tracing is initialized but no OTLP destination is configured yet.
10
+ *
11
+ * Fire-and-forget: export() never blocks the instrumented code path.
12
+ * Failures are swallowed — a span dropped from the queue is preferable to
13
+ * crashing the application.
14
+ */
15
+ export declare class SQLiteSpanExporter implements SpanExporter {
16
+ private readonly queue;
17
+ private readonly enabled;
18
+ private readonly onEnqueued?;
19
+ /**
20
+ * @param onEnqueued - called after spans are persisted, so the pipeline can
21
+ * opportunistically attempt delivery (store-first, then try to send).
22
+ */
23
+ constructor(queue: SpanQueueRepository, enabled?: boolean, onEnqueued?: () => void);
24
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
25
+ shutdown(): Promise<void>;
26
+ }
@@ -0,0 +1,47 @@
1
+ import { ExportResultCode } from "@opentelemetry/core";
2
+ import { serializeSpan } from "./span_serializer";
3
+ /**
4
+ * OTel SpanExporter that persists spans to SQLite instead of sending them
5
+ * directly over the network. The flush cron drains the queue when online.
6
+ *
7
+ * Pass `enabled: false` to register the provider without storing anything —
8
+ * useful when tracing is initialized but no OTLP destination is configured yet.
9
+ *
10
+ * Fire-and-forget: export() never blocks the instrumented code path.
11
+ * Failures are swallowed — a span dropped from the queue is preferable to
12
+ * crashing the application.
13
+ */
14
+ export class SQLiteSpanExporter {
15
+ queue;
16
+ enabled;
17
+ onEnqueued;
18
+ /**
19
+ * @param onEnqueued - called after spans are persisted, so the pipeline can
20
+ * opportunistically attempt delivery (store-first, then try to send).
21
+ */
22
+ constructor(queue, enabled = true, onEnqueued) {
23
+ this.queue = queue;
24
+ this.enabled = enabled;
25
+ this.onEnqueued = onEnqueued;
26
+ }
27
+ export(spans, resultCallback) {
28
+ if (!this.enabled) {
29
+ resultCallback({ code: ExportResultCode.SUCCESS });
30
+ return;
31
+ }
32
+ const persist = async () => {
33
+ for (const span of spans) {
34
+ await this.queue.enqueue(serializeSpan(span));
35
+ }
36
+ };
37
+ persist()
38
+ .then(() => {
39
+ this.onEnqueued?.();
40
+ resultCallback({ code: ExportResultCode.SUCCESS });
41
+ })
42
+ .catch(() => resultCallback({ code: ExportResultCode.FAILED }));
43
+ }
44
+ shutdown() {
45
+ return Promise.resolve();
46
+ }
47
+ }
@@ -0,0 +1,22 @@
1
+ import type { SpanQueueRepository } from "./span_queue_repository";
2
+ export interface TracingResourceConfig {
3
+ serviceName?: string;
4
+ serviceVersion?: string;
5
+ deploymentEnvironment?: string;
6
+ /** Extra OTel resource attributes (device.*, os.*, tenant, …). */
7
+ extra?: Record<string, string>;
8
+ }
9
+ /**
10
+ * @param queue - repository used by the exporter to buffer spans
11
+ * @param persist - when false the TracerProvider is registered (decorators work)
12
+ * but spans are not written to SQLite; pass true only when an
13
+ * OTLP destination is configured and flush is active
14
+ * @param onEnqueued - called after spans are buffered, so the pipeline can
15
+ * opportunistically attempt delivery (store-first)
16
+ * @param resourceConfig - OTel resource attributes; without `serviceName` spans appear
17
+ * as `unknown_service` in Grafana/Tempo
18
+ */
19
+ export declare function initTracing(queue: SpanQueueRepository, persist?: boolean, onEnqueued?: () => void, resourceConfig?: TracingResourceConfig): void;
20
+ /** @internal — test use only */
21
+ export declare function _resetTracing(): void;
22
+ export declare function getTracer(name: string): import("@opentelemetry/api").Tracer;
@@ -0,0 +1,45 @@
1
+ import { trace } from "@opentelemetry/api";
2
+ import { resourceFromAttributes } from "@opentelemetry/resources";
3
+ import { BasicTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
4
+ import { SQLiteSpanExporter } from "./sqlite_span_exporter";
5
+ let _provider = null;
6
+ /**
7
+ * @param queue - repository used by the exporter to buffer spans
8
+ * @param persist - when false the TracerProvider is registered (decorators work)
9
+ * but spans are not written to SQLite; pass true only when an
10
+ * OTLP destination is configured and flush is active
11
+ * @param onEnqueued - called after spans are buffered, so the pipeline can
12
+ * opportunistically attempt delivery (store-first)
13
+ * @param resourceConfig - OTel resource attributes; without `serviceName` spans appear
14
+ * as `unknown_service` in Grafana/Tempo
15
+ */
16
+ export function initTracing(queue, persist = true, onEnqueued, resourceConfig) {
17
+ _provider?.shutdown().catch(() => { });
18
+ const rawAttrs = {};
19
+ if (resourceConfig?.serviceName)
20
+ rawAttrs["service.name"] = resourceConfig.serviceName;
21
+ if (resourceConfig?.serviceVersion)
22
+ rawAttrs["service.version"] = resourceConfig.serviceVersion;
23
+ if (resourceConfig?.deploymentEnvironment)
24
+ rawAttrs["deployment.environment"] = resourceConfig.deploymentEnvironment;
25
+ if (resourceConfig?.extra)
26
+ Object.assign(rawAttrs, resourceConfig.extra);
27
+ const resource = Object.keys(rawAttrs).length > 0 ? resourceFromAttributes(rawAttrs) : undefined;
28
+ const provider = new BasicTracerProvider({
29
+ resource,
30
+ spanProcessors: [
31
+ new SimpleSpanProcessor(new SQLiteSpanExporter(queue, persist, onEnqueued)),
32
+ ],
33
+ });
34
+ trace.setGlobalTracerProvider(provider);
35
+ _provider = provider;
36
+ }
37
+ /** @internal — test use only */
38
+ export function _resetTracing() {
39
+ _provider?.shutdown().catch(() => { });
40
+ _provider = null;
41
+ trace.disable();
42
+ }
43
+ export function getTracer(name) {
44
+ return trace.getTracer(name);
45
+ }
@@ -0,0 +1,10 @@
1
+ import type { InterceptorHooks } from "../../interceptor/intercept";
2
+ /**
3
+ * Returns InterceptorHooks that record an OTel span for every intercepted
4
+ * method call. Parent-child relationships are maintained through the framework
5
+ * StackContextManager: the OTel context (with the active span) is stored as a
6
+ * value inside our Context, pushed in `before` and popped in `after`/`onError`.
7
+ *
8
+ * @param tracerName - instrumentation scope name, e.g. "sincpro.mobile.checkout"
9
+ */
10
+ export declare function tracingHooks(tracerName: string): InterceptorHooks;
@@ -0,0 +1,59 @@
1
+ import { ROOT_CONTEXT as OTEL_ROOT, SpanStatusCode, trace } from "@opentelemetry/api";
2
+ import { _getContextManager } from "../../context_manager/index";
3
+ import { OTEL_CTX_KEY } from "../context_keys";
4
+ const _spanMap = new WeakMap();
5
+ // Tracks whether `before` successfully pushed a context entry for this call.
6
+ // Guards `after`/`onError` against popping a context they never pushed.
7
+ const _pushedMap = new WeakSet();
8
+ /**
9
+ * Returns InterceptorHooks that record an OTel span for every intercepted
10
+ * method call. Parent-child relationships are maintained through the framework
11
+ * StackContextManager: the OTel context (with the active span) is stored as a
12
+ * value inside our Context, pushed in `before` and popped in `after`/`onError`.
13
+ *
14
+ * @param tracerName - instrumentation scope name, e.g. "sincpro.mobile.checkout"
15
+ */
16
+ export function tracingHooks(tracerName) {
17
+ return {
18
+ before(call) {
19
+ const manager = _getContextManager();
20
+ const frameworkCtx = manager.active();
21
+ // Read the OTel context carried inside our framework context.
22
+ // Falls back to OTel ROOT if tracing has not been initialized yet.
23
+ const otelCtx = frameworkCtx.get(OTEL_CTX_KEY) ?? OTEL_ROOT;
24
+ const spanName = call.className
25
+ ? `${call.className}.${call.methodName}`
26
+ : call.methodName;
27
+ const span = trace.getTracer(tracerName).startSpan(spanName, undefined, otelCtx);
28
+ _spanMap.set(call, span);
29
+ // Store the updated OTel context (now containing this span) back into
30
+ // the framework context and push it onto the stack so nested calls see
31
+ // this span as their parent.
32
+ const newOtelCtx = trace.setSpan(otelCtx, span);
33
+ manager.push(frameworkCtx.set(OTEL_CTX_KEY, newOtelCtx));
34
+ _pushedMap.add(call);
35
+ },
36
+ after(call) {
37
+ const span = _spanMap.get(call);
38
+ if (!span)
39
+ return;
40
+ _spanMap.delete(call);
41
+ span.setStatus({ code: SpanStatusCode.OK });
42
+ span.end();
43
+ if (_pushedMap.delete(call))
44
+ _getContextManager().pop();
45
+ },
46
+ onError(call, error) {
47
+ const span = _spanMap.get(call);
48
+ if (!span)
49
+ return;
50
+ _spanMap.delete(call);
51
+ if (error instanceof Error)
52
+ span.recordException(error);
53
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
54
+ span.end();
55
+ if (_pushedMap.delete(call))
56
+ _getContextManager().pop();
57
+ },
58
+ };
59
+ }