@teincfood/observability
v0.1.0
Published
Standard frontend observability layer for TeincFood — structured logs, events, metrics and traces for React Native, Tauri and Web. OpenTelemetry-compatible, offline-first, privacy-conscious.
Readme
@teincfood/observability
The standard frontend observability layer for TeincFood. One API across React Native, Tauri/Desktop and Web producing structured logs, events, metrics and traces in an OpenTelemetry-compatible format.
TeincFood apps → @teincfood/observability → OTLP/HTTP+JSON → Collector
→ Loki (logs) / Prometheus (metrics) / Tempo (traces) → Grafana- Offline-first: telemetry buffers in a durable local queue and exports in batches when connectivity returns. Telemetry failures never break the app.
- Private by default: credential/payment/PII-adjacent fields redacted on device; query strings never captured; safe defaults for sampling and limits.
- Independent: zero dependencies on
@teincfood/*or app code (enforced bynpm run check:no-core-deps), so it versions and changes on its own. - Only runtime dependency:
@opentelemetry/api(Trace API + W3C propagation).
Install
npm install @teincfood/observability
# mobile (yarn-only repo): yarn add @teincfood/observabilityInitialize (once, at bootstrap)
React Native (Expo)
// app/_layout.tsx, before/after ensureCoreBootstrap()
import { initObservability, createReactNativeAdapter } from "@teincfood/observability";
import { Platform } from "react-native";
import { mmkvStorage } from "@/utils/storage"; // your KV wrapper
await initObservability({
app: "teincfood-business",
appVersion: "1.0.0",
env: __DEV__ ? "development" : "production",
endpoint: process.env.EXPO_PUBLIC_TELEMETRY_URL, // omit → console (dev) / noop (prod)
apiKey: process.env.EXPO_PUBLIC_TELEMETRY_KEY, // static per-env ingest key (works logged-out)
adapter: createReactNativeAdapter({
storage: mmkvStorage,
platform: Platform.OS === "ios" ? "ios" : "android",
osVersion: String(Platform.Version),
}),
});Tauri desktop
// src/main.tsx, alongside initDebugLogCapture()/bootstrapCore()
import { initObservability, createTauriAdapter } from "@teincfood/observability";
import { tauriStorage } from "./utils/storage"; // your plugin-store wrapper
import { platform as osPlatform, version as osVersion } from "@tauri-apps/plugin-os";
await initObservability({
app: "teincfood-desktop",
appVersion: "1.0.12",
env: import.meta.env.DEV ? "development" : "production",
endpoint: import.meta.env.VITE_TELEMETRY_URL,
apiKey: import.meta.env.VITE_TELEMETRY_KEY,
adapter: createTauriAdapter({
storage: tauriStorage,
os: (await osPlatform()) as "windows" | "macos" | "linux",
osVersion: await osVersion(),
}),
});Web (Next.js)
// src/app/layout.tsx (client component) or after app ready
import { initObservability } from "@teincfood/observability"; // web adapter is default
await initObservability({
app: "teincfood-web",
appVersion: process.env.NEXT_PUBLIC_APP_VERSION ?? "0.1.0",
env: process.env.NEXT_PUBLIC_ENV === "production" ? "production" : "development",
endpoint: process.env.NEXT_PUBLIC_TELEMETRY_URL,
});Use
import { observability } from "@teincfood/observability";
// Structured logs (never build strings containing the data)
observability.logger.info("Order created", { orderId, source: "pos" });
observability.logger.error("Payment failed", { orderId, code });
// Domain events (generic mechanism — see docs/events.md for naming)
observability.event("checkout.completed", { orderId, total });
observability.event("sync.failed", { scope, error });
// Metrics (your own names — no library changes needed)
observability.counter("api.request.errors").add(1, { route: "/orders" });
observability.gauge("offline.queue.depth").set(depth);
observability.histogram("checkout.duration", { unit: "ms" }).record(ms);
// Traces (traceparent propagates automatically on instrumented requests)
await observability.trace("checkout", async (span) => {
span.setAttribute("orderId", orderId);
await placeOrder();
});
// Business context (stamped on every record; clear on logout)
observability.setContext({ businessId, branchId, terminalId, userId });
observability.setContext({ clearUser: true, clearBusiness: true });
// Runtime log level (e.g. driven by a remote troubleshooting flag)
observability.setLevel("DEBUG");
// Manual error capture (global handlers are installed by default)
observability.captureException(error, { orderId });HTTP instrumentation
// fetch
const res = await observability.http()("https://api…/orders", { method: "POST", body });
// axios (mobile + desktop share this pattern)
api.interceptors.request.use(...); // see docs — full snippet below
const ax = observability.axiosInterceptors();
api.interceptors.request.use(ax.request, ax.requestError);
api.interceptors.response.use(ax.response, ax.responseError);Captured: method, normalized route (/orders/{id} — ids collapsed, query
strings dropped), status, duration, traceparent. Never captured: headers,
bodies, query strings, tokens. The telemetry endpoint itself is always
excluded (no feedback loops).
Configuration
| Option | Default |
|---|---|
| minLevel | INFO in production, DEBUG otherwise (overridable at runtime via setLevel) |
| sampling.traces | 0.1 prod, 1 dev/staging — errors bypass sampling |
| sampling.logs | 1 — errors/fatals bypass sampling |
| signals logs/events/metrics/spans | all on; toggle per build |
| limits | queue 1000 records · 64 KB/record · 256 KB/batch · 5 attempts · 7-day retention · 10 s flush |
| redact.extraDenyKeys / allowKeys | extend (or lock down to) the built-in denylist |
No production endpoints are hard-coded — endpoint comes from app config.
When endpoint is omitted: dev logs to console, prod collects nowhere.
Telemetry schema
Every record carries the envelope in docs/telemetry-schema.md:
app, app_version, env, platform, installation_id, session_id, event_id,
timestamp, user/business/branch/terminal ids, trace/span ids. Backend and
DevOps: that document is your Grafana/Loki/Prometheus field reference.
Docs
docs/telemetry-schema.md— field reference for backend/DevOpsdocs/events.md— recommended domain-event names + metric/span cookbookdocs/backend-contract.md— OTLP endpoints, JWT auth, collector contractdocs/configuration.md— full config reference (limits, sampling, redaction)
Development
npm test # vitest
npm run typecheck # tsc --noEmit
npm run build # tsc → dist/
npm run check:no-core-deps # independence guard (CI)Independence rule: src/ must never import @teincfood/* siblings or
platform SDKs (RN/Tauri/Next) outside src/platform/*. Adapters are injected;
platform packages only read globalThis and injected storage.
