@sincpro/mobile 0.3.1 → 0.4.2
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/README.md +234 -2
- package/dist/entrypoints/cron/EBuiltinCron.d.ts +4 -0
- package/dist/entrypoints/cron/EBuiltinCron.js +5 -0
- package/dist/entrypoints/cron/checkNetworkStatus.cron.js +2 -1
- package/dist/entrypoints/db/migrations.d.ts +3 -1
- package/dist/entrypoints/db/migrations.js +47 -0
- package/dist/entrypoints/queue/QueueProcessor.js +1 -25
- package/dist/entrypoints/queue/subscriber_filter.d.ts +9 -0
- package/dist/entrypoints/queue/subscriber_filter.js +25 -0
- package/dist/entrypoints/ui/common_provider.js +2 -2
- package/dist/framework/base_module.d.ts +1 -0
- package/dist/framework/base_module.js +6 -1
- package/dist/framework/createApp.d.ts +14 -0
- package/dist/framework/createApp.js +6 -0
- package/dist/framework/kernel.d.ts +2 -1
- package/dist/framework/kernel.js +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/infrastructure/context_manager/context.d.ts +16 -0
- package/dist/infrastructure/context_manager/context.js +37 -0
- package/dist/infrastructure/context_manager/context_api.d.ts +28 -0
- package/dist/infrastructure/context_manager/context_api.js +55 -0
- package/dist/infrastructure/context_manager/context_key.d.ts +15 -0
- package/dist/infrastructure/context_manager/context_key.js +3 -0
- package/dist/infrastructure/context_manager/context_manager_facade.d.ts +107 -0
- package/dist/infrastructure/context_manager/context_manager_facade.js +113 -0
- package/dist/infrastructure/context_manager/decorators/propagate_context.d.ts +24 -0
- package/dist/infrastructure/context_manager/decorators/propagate_context.js +52 -0
- package/dist/infrastructure/context_manager/decorators/with_context.d.ts +34 -0
- package/dist/infrastructure/context_manager/decorators/with_context.js +44 -0
- package/dist/infrastructure/context_manager/index.d.ts +11 -0
- package/dist/infrastructure/context_manager/index.js +12 -0
- package/dist/infrastructure/context_manager/managed_context.d.ts +87 -0
- package/dist/infrastructure/context_manager/managed_context.js +144 -0
- package/dist/infrastructure/context_manager/stack_context_manager.d.ts +39 -0
- package/dist/infrastructure/context_manager/stack_context_manager.js +56 -0
- package/dist/infrastructure/database/connector.js +10 -1
- package/dist/infrastructure/interceptor/index.d.ts +4 -0
- package/dist/infrastructure/interceptor/index.js +2 -0
- package/dist/infrastructure/interceptor/intercept.d.ts +57 -0
- package/dist/infrastructure/interceptor/intercept.js +112 -0
- package/dist/infrastructure/interceptor/logging_hooks.d.ts +22 -0
- package/dist/infrastructure/interceptor/logging_hooks.js +27 -0
- package/dist/infrastructure/logger.d.ts +11 -16
- package/dist/infrastructure/logger.js +61 -18
- package/dist/infrastructure/telemetry/buffer_registry.d.ts +15 -0
- package/dist/infrastructure/telemetry/buffer_registry.js +26 -0
- package/dist/infrastructure/telemetry/connectivity_state.d.ts +20 -0
- package/dist/infrastructure/telemetry/connectivity_state.js +28 -0
- package/dist/infrastructure/telemetry/context_keys.d.ts +7 -0
- package/dist/infrastructure/telemetry/context_keys.js +7 -0
- package/dist/infrastructure/telemetry/fetch_with_timeout.d.ts +8 -0
- package/dist/infrastructure/telemetry/fetch_with_timeout.js +17 -0
- package/dist/infrastructure/telemetry/flush_telemetry.d.ts +40 -0
- package/dist/infrastructure/telemetry/flush_telemetry.js +72 -0
- package/dist/infrastructure/telemetry/index.d.ts +17 -0
- package/dist/infrastructure/telemetry/index.js +17 -0
- package/dist/infrastructure/telemetry/init_telemetry.d.ts +21 -0
- package/dist/infrastructure/telemetry/init_telemetry.js +85 -0
- package/dist/infrastructure/telemetry/logging/flush_job.d.ts +44 -0
- package/dist/infrastructure/telemetry/logging/flush_job.js +69 -0
- package/dist/infrastructure/telemetry/logging/log_queue_repository.d.ts +76 -0
- package/dist/infrastructure/telemetry/logging/log_queue_repository.js +147 -0
- package/dist/infrastructure/telemetry/logging/loki_client.d.ts +32 -0
- package/dist/infrastructure/telemetry/logging/loki_client.js +118 -0
- package/dist/infrastructure/telemetry/logging/loki_registry.d.ts +12 -0
- package/dist/infrastructure/telemetry/logging/loki_registry.js +17 -0
- package/dist/infrastructure/telemetry/telemetry_flush_worker.d.ts +35 -0
- package/dist/infrastructure/telemetry/telemetry_flush_worker.js +66 -0
- package/dist/infrastructure/telemetry/telemetry_signal.d.ts +18 -0
- package/dist/infrastructure/telemetry/telemetry_signal.js +33 -0
- package/dist/infrastructure/telemetry/tracing/active_span.d.ts +22 -0
- package/dist/infrastructure/telemetry/tracing/active_span.js +40 -0
- package/dist/infrastructure/telemetry/tracing/otlp_client.d.ts +13 -0
- package/dist/infrastructure/telemetry/tracing/otlp_client.js +34 -0
- package/dist/infrastructure/telemetry/tracing/otlp_serializer.d.ts +67 -0
- package/dist/infrastructure/telemetry/tracing/otlp_serializer.js +101 -0
- package/dist/infrastructure/telemetry/tracing/span_flush_job.d.ts +25 -0
- package/dist/infrastructure/telemetry/tracing/span_flush_job.js +42 -0
- package/dist/infrastructure/telemetry/tracing/span_queue_repository.d.ts +82 -0
- package/dist/infrastructure/telemetry/tracing/span_queue_repository.js +134 -0
- package/dist/infrastructure/telemetry/tracing/span_sampler.d.ts +40 -0
- package/dist/infrastructure/telemetry/tracing/span_sampler.js +75 -0
- package/dist/infrastructure/telemetry/tracing/span_serializer.d.ts +48 -0
- package/dist/infrastructure/telemetry/tracing/span_serializer.js +36 -0
- package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.d.ts +26 -0
- package/dist/infrastructure/telemetry/tracing/sqlite_span_exporter.js +47 -0
- package/dist/infrastructure/telemetry/tracing/tracer.d.ts +22 -0
- package/dist/infrastructure/telemetry/tracing/tracer.js +45 -0
- package/dist/infrastructure/telemetry/tracing/tracing_hooks.d.ts +10 -0
- package/dist/infrastructure/telemetry/tracing/tracing_hooks.js +59 -0
- package/dist/infrastructure/telemetry/tracing_facade.d.ts +50 -0
- package/dist/infrastructure/telemetry/tracing_facade.js +139 -0
- package/dist/infrastructure/telemetry/types.d.ts +120 -0
- package/dist/infrastructure/telemetry/types.js +1 -0
- package/dist/infrastructure/workers/CronWorker.d.ts +10 -2
- package/dist/infrastructure/workers/CronWorker.js +18 -5
- package/dist/infrastructure/workers/index.d.ts +1 -1
- package/dist/ui/components/molecules/TimeZoneSelectorModal.js +7 -1
- package/package.json +17 -11
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Attributes, Span, SpanContext } from "@opentelemetry/api";
|
|
2
|
+
import { bufferStats } from "./buffer_registry";
|
|
3
|
+
import { initTelemetry } from "./init_telemetry";
|
|
4
|
+
import { getTracer as _getTracer } from "./tracing/tracer";
|
|
5
|
+
export declare function withSpan<T>(name: string, fn: (span: Span) => T | Promise<T>, opts?: {
|
|
6
|
+
attributes?: Attributes;
|
|
7
|
+
tracerName?: string;
|
|
8
|
+
}): Promise<T>;
|
|
9
|
+
export interface TraceOptions {
|
|
10
|
+
name?: string;
|
|
11
|
+
attributes?: Attributes;
|
|
12
|
+
tracerName?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function Trace(opts?: TraceOptions): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
15
|
+
export declare function TraceClass(tracerName: string): <T extends new (...args: never[]) => object>(target: T) => T;
|
|
16
|
+
export declare const Tracing: {
|
|
17
|
+
/** Initializes telemetry (Loki + OTel). Call once at app startup. */
|
|
18
|
+
readonly init: typeof initTelemetry;
|
|
19
|
+
/** Wraps a block of code in a span. Handles start/end/error automatically. */
|
|
20
|
+
readonly withSpan: typeof withSpan;
|
|
21
|
+
/** Returns the active SpanContext ({ traceId, spanId }) or null if no span is active. */
|
|
22
|
+
readonly activeSpan: () => SpanContext | null;
|
|
23
|
+
/** Log suffix ready to append: " trace_id=xxx span_id=xxx" or "". */
|
|
24
|
+
readonly logSuffix: () => string;
|
|
25
|
+
/**
|
|
26
|
+
* Current size and drop pressure of the offline telemetry buffers.
|
|
27
|
+
* A rising `dropped` count means the device has been offline long enough
|
|
28
|
+
* to lose data. Surface it in health checks / dashboards.
|
|
29
|
+
*/
|
|
30
|
+
readonly bufferStats: typeof bufferStats;
|
|
31
|
+
/** Direct access to the OTel tracer for advanced use cases. */
|
|
32
|
+
readonly getTracer: typeof _getTracer;
|
|
33
|
+
/**
|
|
34
|
+
* Method decorator — creates a span per invocation.
|
|
35
|
+
* @example
|
|
36
|
+
* class CheckoutUseCase {
|
|
37
|
+
* \@Tracing.Trace()
|
|
38
|
+
* async process(order: Order) { ... }
|
|
39
|
+
* }
|
|
40
|
+
*/
|
|
41
|
+
readonly Trace: typeof Trace;
|
|
42
|
+
/**
|
|
43
|
+
* Class decorator — creates a span per public method and propagates context.
|
|
44
|
+
* Span name: `"<tracerName>.<methodName>"`.
|
|
45
|
+
* @example
|
|
46
|
+
* \@Tracing.TraceClass("checkout")
|
|
47
|
+
* class CheckoutUseCase { ... }
|
|
48
|
+
*/
|
|
49
|
+
readonly TraceClass: typeof TraceClass;
|
|
50
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ROOT_CONTEXT as OTEL_ROOT, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
import { _getContextManager, PropagateContext, } from "../../infrastructure/context_manager/index";
|
|
3
|
+
import { interceptClass } from "../../infrastructure/interceptor/intercept";
|
|
4
|
+
import { bufferStats } from "./buffer_registry";
|
|
5
|
+
import { OTEL_CTX_KEY } from "./context_keys";
|
|
6
|
+
import { initTelemetry } from "./init_telemetry";
|
|
7
|
+
import { activeTraceLabel, getActiveSpanContext } from "./tracing/active_span";
|
|
8
|
+
import { getTracer as _getTracer } from "./tracing/tracer";
|
|
9
|
+
import { tracingHooks } from "./tracing/tracing_hooks";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// withSpan — manual span block with automatic lifecycle
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export async function withSpan(name, fn, opts) {
|
|
14
|
+
const manager = _getContextManager();
|
|
15
|
+
const frameworkCtx = manager.active();
|
|
16
|
+
const otelCtx = frameworkCtx.get(OTEL_CTX_KEY) ?? OTEL_ROOT;
|
|
17
|
+
const tracer = trace.getTracer(opts?.tracerName ?? "sincpro.mobile");
|
|
18
|
+
const span = tracer.startSpan(name, { attributes: opts?.attributes }, otelCtx);
|
|
19
|
+
const newOtelCtx = trace.setSpan(otelCtx, span);
|
|
20
|
+
manager.push(frameworkCtx.set(OTEL_CTX_KEY, newOtelCtx));
|
|
21
|
+
try {
|
|
22
|
+
const result = await Promise.resolve(fn(span));
|
|
23
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof Error)
|
|
28
|
+
span.recordException(error);
|
|
29
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
span.end();
|
|
34
|
+
manager.pop();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function Trace(opts) {
|
|
38
|
+
return function (target, propertyKey, descriptor) {
|
|
39
|
+
const original = descriptor.value;
|
|
40
|
+
const className = target.constructor.name ?? "";
|
|
41
|
+
descriptor.value = function (...args) {
|
|
42
|
+
const spanName = opts?.name ?? (className ? `${className}.${propertyKey}` : propertyKey);
|
|
43
|
+
const manager = _getContextManager();
|
|
44
|
+
const frameworkCtx = manager.active();
|
|
45
|
+
const otelCtx = frameworkCtx.get(OTEL_CTX_KEY) ?? OTEL_ROOT;
|
|
46
|
+
const tracer = trace.getTracer(opts?.tracerName ?? "sincpro.mobile");
|
|
47
|
+
const span = tracer.startSpan(spanName, { attributes: opts?.attributes }, otelCtx);
|
|
48
|
+
manager.push(frameworkCtx.set(OTEL_CTX_KEY, trace.setSpan(otelCtx, span)));
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
result = original.apply(this, args);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Sync method threw before returning — record and clean up immediately.
|
|
55
|
+
if (error instanceof Error)
|
|
56
|
+
span.recordException(error);
|
|
57
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
|
|
58
|
+
span.end();
|
|
59
|
+
manager.pop();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (result instanceof Promise) {
|
|
63
|
+
return result.then((value) => {
|
|
64
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
65
|
+
span.end();
|
|
66
|
+
manager.pop();
|
|
67
|
+
return value;
|
|
68
|
+
}, (error) => {
|
|
69
|
+
if (error instanceof Error)
|
|
70
|
+
span.recordException(error);
|
|
71
|
+
span.setStatus({
|
|
72
|
+
code: SpanStatusCode.ERROR,
|
|
73
|
+
message: error?.message,
|
|
74
|
+
});
|
|
75
|
+
span.end();
|
|
76
|
+
manager.pop();
|
|
77
|
+
throw error;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Sync success path.
|
|
81
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
82
|
+
span.end();
|
|
83
|
+
manager.pop();
|
|
84
|
+
return result;
|
|
85
|
+
};
|
|
86
|
+
return descriptor;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// @TraceClass — class decorator: PropagateContext + span per method
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export function TraceClass(tracerName) {
|
|
93
|
+
return function (target) {
|
|
94
|
+
const traced = interceptClass(target, tracingHooks(tracerName));
|
|
95
|
+
return PropagateContext(traced);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Public facade
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
export const Tracing = {
|
|
102
|
+
/** Initializes telemetry (Loki + OTel). Call once at app startup. */
|
|
103
|
+
init: initTelemetry,
|
|
104
|
+
/** Wraps a block of code in a span. Handles start/end/error automatically. */
|
|
105
|
+
withSpan,
|
|
106
|
+
/** Returns the active SpanContext ({ traceId, spanId }) or null if no span is active. */
|
|
107
|
+
activeSpan() {
|
|
108
|
+
return getActiveSpanContext();
|
|
109
|
+
},
|
|
110
|
+
/** Log suffix ready to append: " trace_id=xxx span_id=xxx" or "". */
|
|
111
|
+
logSuffix() {
|
|
112
|
+
return activeTraceLabel();
|
|
113
|
+
},
|
|
114
|
+
/**
|
|
115
|
+
* Current size and drop pressure of the offline telemetry buffers.
|
|
116
|
+
* A rising `dropped` count means the device has been offline long enough
|
|
117
|
+
* to lose data. Surface it in health checks / dashboards.
|
|
118
|
+
*/
|
|
119
|
+
bufferStats,
|
|
120
|
+
/** Direct access to the OTel tracer for advanced use cases. */
|
|
121
|
+
getTracer: _getTracer,
|
|
122
|
+
/**
|
|
123
|
+
* Method decorator — creates a span per invocation.
|
|
124
|
+
* @example
|
|
125
|
+
* class CheckoutUseCase {
|
|
126
|
+
* \@Tracing.Trace()
|
|
127
|
+
* async process(order: Order) { ... }
|
|
128
|
+
* }
|
|
129
|
+
*/
|
|
130
|
+
Trace,
|
|
131
|
+
/**
|
|
132
|
+
* Class decorator — creates a span per public method and propagates context.
|
|
133
|
+
* Span name: `"<tracerName>.<methodName>"`.
|
|
134
|
+
* @example
|
|
135
|
+
* \@Tracing.TraceClass("checkout")
|
|
136
|
+
* class CheckoutUseCase { ... }
|
|
137
|
+
*/
|
|
138
|
+
TraceClass,
|
|
139
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export type LokiAuth = {
|
|
2
|
+
type: "basic";
|
|
3
|
+
username: string;
|
|
4
|
+
password: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "bearer";
|
|
7
|
+
token: string;
|
|
8
|
+
};
|
|
9
|
+
export interface LokiConfig {
|
|
10
|
+
/** Base URL of the Loki instance, e.g. "https://loki.myserver.com" */
|
|
11
|
+
endpoint: string;
|
|
12
|
+
/** Labels attached to every log stream (app, env, tenant, …) */
|
|
13
|
+
labels: Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Standard auth shortcut (Basic / Bearer). For anything else use `headers`.
|
|
16
|
+
* Omit both for unauthenticated (internal/self-hosted) setups.
|
|
17
|
+
*/
|
|
18
|
+
auth?: LokiAuth;
|
|
19
|
+
/**
|
|
20
|
+
* Arbitrary headers added to every Loki request — for gateways/proxies that
|
|
21
|
+
* authenticate with custom keys (e.g. `{ "api-key": "…" }`,
|
|
22
|
+
* `{ "sincpro-api-key": "…" }`, `{ "X-Scope-OrgID": "tenant-1" }`).
|
|
23
|
+
* Merged after `auth`, so an explicit `Authorization` here overrides `auth`.
|
|
24
|
+
*/
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface OtlpConfig {
|
|
28
|
+
/** OTLP/HTTP collector base URL, e.g. "https://alloy.myserver.com" */
|
|
29
|
+
endpoint: string;
|
|
30
|
+
/** Optional headers (Authorization, X-Scope-OrgID, …) */
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface FlushConfig {
|
|
34
|
+
/**
|
|
35
|
+
* Background safety-net interval in minutes (≥15 → real OS background task).
|
|
36
|
+
* Set to 0 to disable the cron entirely and rely only on the production
|
|
37
|
+
* signal (and connectivity events, if enabled). Default 15.
|
|
38
|
+
*/
|
|
39
|
+
backgroundIntervalMin?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to the app's InternetIsUp/Down events to drain the backlog the
|
|
42
|
+
* moment connectivity returns. Off by default — the framework does not
|
|
43
|
+
* require the app's connectivity cron. Default false.
|
|
44
|
+
*/
|
|
45
|
+
onConnectivityEvents?: boolean;
|
|
46
|
+
/** HTTP send timeout in ms (fail fast instead of hanging). Default 4000. */
|
|
47
|
+
sendTimeoutMs?: number;
|
|
48
|
+
/** Coalescing window in ms for production-triggered flushes. Default 3000. */
|
|
49
|
+
debounceMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface TelemetryConfig {
|
|
52
|
+
/**
|
|
53
|
+
* Name of this app/service as it appears in Grafana/Tempo/Jaeger.
|
|
54
|
+
* Becomes the OTel resource `service.name` attribute on every span.
|
|
55
|
+
* Without it, spans are grouped under `unknown_service`, making multi-app
|
|
56
|
+
* setups impossible to filter. Example: `"pos-mobile"`, `"warehouse-mobile"`.
|
|
57
|
+
*/
|
|
58
|
+
serviceName?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Semver of the deployed app. Becomes `service.version` on every span.
|
|
61
|
+
* Enables filtering by version in Grafana Tempo and alerting on regressions.
|
|
62
|
+
* Example: `"1.4.2"`.
|
|
63
|
+
*/
|
|
64
|
+
serviceVersion?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Deployment tier. Becomes `deployment.environment` on every span.
|
|
67
|
+
* Separates dev/staging/production traces in Grafana without label tricks.
|
|
68
|
+
* Example: `"production"`, `"staging"`, `"development"`.
|
|
69
|
+
*/
|
|
70
|
+
deploymentEnvironment?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Extra OTel resource attributes merged onto every span. Use for device,
|
|
73
|
+
* OS, tenant, or any dimension not covered above.
|
|
74
|
+
* Common keys (OTel semantic conventions):
|
|
75
|
+
* `"device.model.name"` — e.g. "iPhone 16 Pro"
|
|
76
|
+
* `"os.type"` — `Platform.OS` ("ios" | "android")
|
|
77
|
+
* `"os.version"` — `String(Platform.Version)`
|
|
78
|
+
* `"device.id"` — opaque installation ID
|
|
79
|
+
* Values must be strings (OTel resource attributes are always string here).
|
|
80
|
+
*/
|
|
81
|
+
resourceAttributes?: Record<string, string>;
|
|
82
|
+
/** Omit to disable log shipping (e.g. traces-only setup). */
|
|
83
|
+
loki?: LokiConfig;
|
|
84
|
+
/** Omit to disable span flushing (e.g. logs-only setup). */
|
|
85
|
+
otlp?: OtlpConfig;
|
|
86
|
+
/** Delivery tuning — triggers, timeout, debounce. Sensible defaults if omitted. */
|
|
87
|
+
flush?: FlushConfig;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Snapshot of a bounded telemetry buffer.
|
|
91
|
+
*
|
|
92
|
+
* Telemetry is best-effort and bounded: when a device stays offline, the
|
|
93
|
+
* buffer fills and the oldest data is evicted. These numbers make that loss
|
|
94
|
+
* observable instead of silent. Business-critical data must NOT live here.
|
|
95
|
+
*/
|
|
96
|
+
export interface BufferStats {
|
|
97
|
+
/** Rows currently buffered. */
|
|
98
|
+
rows: number;
|
|
99
|
+
/** Lower-bound estimate of buffered payload text in bytes (sum of field lengths). */
|
|
100
|
+
approxBytes: number;
|
|
101
|
+
/**
|
|
102
|
+
* Rows EVICTED after being buffered, since process start. This is the buffer
|
|
103
|
+
* back-pressure indicator — a rising value means data was buffered then lost
|
|
104
|
+
* because the device stayed offline past the cap.
|
|
105
|
+
*/
|
|
106
|
+
dropped: number;
|
|
107
|
+
/**
|
|
108
|
+
* Items refused at INGEST by head sampling (never buffered), since process
|
|
109
|
+
* start. Distinct from `dropped`: a healthy sampler shedding load under
|
|
110
|
+
* pressure raises this without signalling buffer distress. 0 for log buffers
|
|
111
|
+
* (logs are not head-sampled).
|
|
112
|
+
*/
|
|
113
|
+
sampled: number;
|
|
114
|
+
}
|
|
115
|
+
export interface TelemetryBufferStats {
|
|
116
|
+
/** Log queue stats, or null when log shipping is disabled. */
|
|
117
|
+
logs: BufferStats | null;
|
|
118
|
+
/** Span queue stats, or null when tracing is disabled. */
|
|
119
|
+
spans: BufferStats | null;
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
export type CronJob = () => Promise<void>;
|
|
2
|
+
export interface CronWorkerOpts {
|
|
3
|
+
requiresAuth?: boolean;
|
|
4
|
+
label?: string;
|
|
5
|
+
showToast?: boolean;
|
|
6
|
+
}
|
|
2
7
|
export declare class CronWorker {
|
|
3
8
|
readonly taskName: string;
|
|
4
9
|
private readonly job;
|
|
5
10
|
private readonly intervalMin;
|
|
6
|
-
readonly requiresAuth: boolean;
|
|
7
11
|
private stopFn?;
|
|
8
12
|
private isRegistering;
|
|
9
13
|
private isExecuting;
|
|
10
14
|
private registrationError?;
|
|
11
|
-
|
|
15
|
+
readonly requiresAuth: boolean;
|
|
16
|
+
label?: string;
|
|
17
|
+
private showToast;
|
|
18
|
+
constructor(taskName: string, job: CronJob, intervalMin?: number, opts?: CronWorkerOpts);
|
|
19
|
+
configure(opts: Partial<CronWorkerOpts>): void;
|
|
12
20
|
/**
|
|
13
21
|
* Registers a task with a specified interval and execution logic.
|
|
14
22
|
* If the interval is less than 15 minutes, a repeating job is created using a defined stop function.
|
|
@@ -6,16 +6,26 @@ export class CronWorker {
|
|
|
6
6
|
taskName;
|
|
7
7
|
job;
|
|
8
8
|
intervalMin;
|
|
9
|
-
requiresAuth;
|
|
10
9
|
stopFn;
|
|
11
10
|
isRegistering = false;
|
|
12
11
|
isExecuting = false;
|
|
13
12
|
registrationError;
|
|
14
|
-
|
|
13
|
+
requiresAuth;
|
|
14
|
+
label;
|
|
15
|
+
showToast;
|
|
16
|
+
constructor(taskName, job, intervalMin = 15, opts = {}) {
|
|
15
17
|
this.taskName = taskName;
|
|
16
18
|
this.job = job;
|
|
17
19
|
this.intervalMin = intervalMin;
|
|
18
|
-
this.requiresAuth = requiresAuth;
|
|
20
|
+
this.requiresAuth = opts.requiresAuth ?? false;
|
|
21
|
+
this.label = opts.label;
|
|
22
|
+
this.showToast = opts.showToast ?? false;
|
|
23
|
+
}
|
|
24
|
+
configure(opts) {
|
|
25
|
+
if (opts.label !== undefined)
|
|
26
|
+
this.label = opts.label;
|
|
27
|
+
if (opts.showToast !== undefined)
|
|
28
|
+
this.showToast = opts.showToast;
|
|
19
29
|
}
|
|
20
30
|
/**
|
|
21
31
|
* Registers a task with a specified interval and execution logic.
|
|
@@ -106,7 +116,9 @@ export class CronWorker {
|
|
|
106
116
|
async runWithEvents(job) {
|
|
107
117
|
this.isExecuting = true;
|
|
108
118
|
const startTime = Date.now();
|
|
109
|
-
|
|
119
|
+
if (this.showToast) {
|
|
120
|
+
UIEventBus.emit("CRON_START", { task: this.taskName, label: this.label });
|
|
121
|
+
}
|
|
110
122
|
try {
|
|
111
123
|
await Promise.resolve(job());
|
|
112
124
|
const duration = Date.now() - startTime;
|
|
@@ -119,7 +131,8 @@ export class CronWorker {
|
|
|
119
131
|
}
|
|
120
132
|
finally {
|
|
121
133
|
this.isExecuting = false;
|
|
122
|
-
|
|
134
|
+
if (this.showToast)
|
|
135
|
+
UIEventBus.emit("CRON_END", { task: this.taskName });
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
/**
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CronJob, CronWorker } from "./CronWorker";
|
|
1
|
+
export { CronJob, CronWorker, CronWorkerOpts } from "./CronWorker";
|
|
2
2
|
export { EventBus, EventName } from "./EventBus";
|
|
@@ -4,7 +4,7 @@ import { Display } from "@sincpro/mobile-ui/Display";
|
|
|
4
4
|
import { Form } from "@sincpro/mobile-ui/Form";
|
|
5
5
|
import { useTheme } from "@sincpro/mobile-ui/theme";
|
|
6
6
|
import { Typography } from "@sincpro/mobile-ui/Typography";
|
|
7
|
-
import { useMemo, useState } from "react";
|
|
7
|
+
import { useEffect, useMemo, useState } from "react";
|
|
8
8
|
import { TextInput, TouchableOpacity, View } from "react-native";
|
|
9
9
|
const TIMEZONES_LOCALES = {
|
|
10
10
|
bolivia: { timezone: "America/La_Paz", locale: "es-BO" },
|
|
@@ -27,6 +27,12 @@ function TimeZoneSelectorModal({ visible, onClose, onSelect, currentTimezone, })
|
|
|
27
27
|
const theme = useTheme();
|
|
28
28
|
const [query, setQuery] = useState("");
|
|
29
29
|
const [selectedTimezone, setSelectedTimezone] = useState(currentTimezone || null);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (visible) {
|
|
32
|
+
setQuery("");
|
|
33
|
+
setSelectedTimezone(currentTimezone || null);
|
|
34
|
+
}
|
|
35
|
+
}, [visible]);
|
|
30
36
|
const timezoneList = useMemo(() => {
|
|
31
37
|
return Object.values(TIMEZONES_LOCALES).map((tz) => ({
|
|
32
38
|
...tz,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/mobile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Sincpro Mobile framework core: offline-first infra (DB, queue/events, cron), DDD patterns, adapters, AppShell and module-driven composition for React Native",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"author": "Gutierrez, Andres <andres.gutierrez@sincpro.com.bo>",
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@opentelemetry/api": "^1.9.0",
|
|
31
|
+
"@opentelemetry/core": "^2.8.0",
|
|
32
|
+
"@opentelemetry/resources": "^2.8.0",
|
|
33
|
+
"@opentelemetry/sdk-trace-base": "^2.8.0",
|
|
30
34
|
"async-mutex": "^0.5.0",
|
|
31
35
|
"big.js": "^7.0.1",
|
|
32
36
|
"dayjs": "^1.11.18",
|
|
@@ -36,6 +40,9 @@
|
|
|
36
40
|
"uuid": "^13.0.0"
|
|
37
41
|
},
|
|
38
42
|
"peerDependencies": {
|
|
43
|
+
"@react-navigation/native": ">=7",
|
|
44
|
+
"@react-navigation/native-stack": ">=7",
|
|
45
|
+
"@sincpro/mobile-ui": "^0.7.2",
|
|
39
46
|
"expo-background-task": "*",
|
|
40
47
|
"expo-clipboard": "*",
|
|
41
48
|
"expo-crypto": "*",
|
|
@@ -55,15 +62,16 @@
|
|
|
55
62
|
"react-native-gesture-handler": "*",
|
|
56
63
|
"react-native-keyboard-controller": "*",
|
|
57
64
|
"react-native-safe-area-context": "*",
|
|
65
|
+
"react-native-screens": "*",
|
|
58
66
|
"react-native-toast-message": "*",
|
|
59
67
|
"react-native-view-shot": "*",
|
|
60
|
-
"react-native-webview": "*"
|
|
61
|
-
"@react-navigation/native": ">=7",
|
|
62
|
-
"@react-navigation/native-stack": ">=7",
|
|
63
|
-
"react-native-screens": "*",
|
|
64
|
-
"@sincpro/mobile-ui": "^0.7.1"
|
|
68
|
+
"react-native-webview": "*"
|
|
65
69
|
},
|
|
66
70
|
"devDependencies": {
|
|
71
|
+
"@opentelemetry/api": "^1.9.0",
|
|
72
|
+
"@react-navigation/native": "^7.0.0",
|
|
73
|
+
"@react-navigation/native-stack": "^7.0.0",
|
|
74
|
+
"@sincpro/mobile-ui": "^0.7.2",
|
|
67
75
|
"@types/big.js": "^6.2.2",
|
|
68
76
|
"@types/react": "~19.1.10",
|
|
69
77
|
"@types/uuid": "^10.0.0",
|
|
@@ -94,16 +102,14 @@
|
|
|
94
102
|
"react-native-keyboard-controller": "1.18.5",
|
|
95
103
|
"react-native-reanimated": "~4.1.1",
|
|
96
104
|
"react-native-safe-area-context": "~5.6.0",
|
|
105
|
+
"react-native-screens": "~4.25.2",
|
|
97
106
|
"react-native-svg": "15.12.1",
|
|
98
107
|
"react-native-toast-message": "^2.3.3",
|
|
99
108
|
"react-native-view-shot": "^4.0.3",
|
|
100
109
|
"react-native-webview": "13.15.0",
|
|
101
|
-
"@react-navigation/native": "^7.0.0",
|
|
102
|
-
"@react-navigation/native-stack": "^7.0.0",
|
|
103
|
-
"react-native-screens": "~4.25.2",
|
|
104
110
|
"tsc-alias": "^1.8.17",
|
|
105
|
-
"
|
|
106
|
-
"
|
|
111
|
+
"tsx": "^4.22.4",
|
|
112
|
+
"typescript": "~5.9.2"
|
|
107
113
|
},
|
|
108
114
|
"exports": {
|
|
109
115
|
".": "./dist/index.js",
|