react-native-otel 0.1.0 → 0.1.4

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 (54) hide show
  1. package/lib/module/context/span-context.js +56 -5
  2. package/lib/module/context/span-context.js.map +1 -1
  3. package/lib/module/core/ids.js +21 -7
  4. package/lib/module/core/ids.js.map +1 -1
  5. package/lib/module/core/meter.js +73 -10
  6. package/lib/module/core/meter.js.map +1 -1
  7. package/lib/module/core/tracer.js +71 -2
  8. package/lib/module/core/tracer.js.map +1 -1
  9. package/lib/module/exporters/console-exporter.js +8 -1
  10. package/lib/module/exporters/console-exporter.js.map +1 -1
  11. package/lib/module/exporters/otlp-http-exporter.js +94 -20
  12. package/lib/module/exporters/otlp-http-exporter.js.map +1 -1
  13. package/lib/module/exporters/wal.js +73 -0
  14. package/lib/module/exporters/wal.js.map +1 -0
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/instrumentation/errors.js +21 -0
  17. package/lib/module/instrumentation/errors.js.map +1 -1
  18. package/lib/module/instrumentation/network.js +11 -0
  19. package/lib/module/instrumentation/network.js.map +1 -1
  20. package/lib/module/sdk.js +24 -2
  21. package/lib/module/sdk.js.map +1 -1
  22. package/lib/typescript/src/context/span-context.d.ts +14 -4
  23. package/lib/typescript/src/context/span-context.d.ts.map +1 -1
  24. package/lib/typescript/src/core/ids.d.ts.map +1 -1
  25. package/lib/typescript/src/core/meter.d.ts +10 -2
  26. package/lib/typescript/src/core/meter.d.ts.map +1 -1
  27. package/lib/typescript/src/core/tracer.d.ts +10 -5
  28. package/lib/typescript/src/core/tracer.d.ts.map +1 -1
  29. package/lib/typescript/src/exporters/console-exporter.d.ts.map +1 -1
  30. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts +11 -2
  31. package/lib/typescript/src/exporters/otlp-http-exporter.d.ts.map +1 -1
  32. package/lib/typescript/src/exporters/types.d.ts +17 -3
  33. package/lib/typescript/src/exporters/types.d.ts.map +1 -1
  34. package/lib/typescript/src/exporters/wal.d.ts +19 -0
  35. package/lib/typescript/src/exporters/wal.d.ts.map +1 -0
  36. package/lib/typescript/src/index.d.ts +2 -0
  37. package/lib/typescript/src/index.d.ts.map +1 -1
  38. package/lib/typescript/src/instrumentation/errors.d.ts.map +1 -1
  39. package/lib/typescript/src/instrumentation/network.d.ts.map +1 -1
  40. package/lib/typescript/src/sdk.d.ts +1 -0
  41. package/lib/typescript/src/sdk.d.ts.map +1 -1
  42. package/package.json +7 -2
  43. package/src/context/span-context.ts +61 -8
  44. package/src/core/ids.ts +21 -7
  45. package/src/core/meter.ts +103 -12
  46. package/src/core/tracer.ts +99 -13
  47. package/src/exporters/console-exporter.ts +18 -4
  48. package/src/exporters/otlp-http-exporter.ts +116 -23
  49. package/src/exporters/types.ts +24 -3
  50. package/src/exporters/wal.ts +86 -0
  51. package/src/index.ts +2 -0
  52. package/src/instrumentation/errors.ts +27 -0
  53. package/src/instrumentation/network.ts +10 -0
  54. package/src/sdk.ts +31 -2
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../../src/instrumentation/errors.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAqBD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD,GAAG,IAAI,CAqDP"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../../src/instrumentation/errors.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAqBD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD,GAAG,IAAI,CAgFP"}
@@ -1 +1 @@
1
- {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/instrumentation/network.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAOxC,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAqED,MAAM,WAAW,2BAA2B;IAM1C,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,2BAA2B;sBAWjB,kBAAkB,GAAG,kBAAkB;yBAmDpC,aAAa,GAAG,aAAa;mBA+BnC,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC;EAqC7C;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAC3C,OAAO,0BAA0B,CAClC,CAAC"}
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/instrumentation/network.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAOxC,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAqED,MAAM,WAAW,2BAA2B;IAM1C,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,2BAA2B;sBAWjB,kBAAkB,GAAG,kBAAkB;yBA6DpC,aAAa,GAAG,aAAa;mBA+BnC,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC;EAqC7C;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAC3C,OAAO,0BAA0B,CAClC,CAAC"}
@@ -43,6 +43,7 @@ declare class OtelSDK {
43
43
  getMeter(): Meter;
44
44
  getSensitiveKeys(): string[];
45
45
  getLogger(): OtelLogger;
46
+ flush(): void;
46
47
  shutdown(): Promise<void>;
47
48
  }
48
49
  export declare const otel: OtelSDK;
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../../src/sdk.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAIvC,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAIlC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,cAAM,OAAO;IACX,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,WAAW,CAAS;IAE5B,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IA4D9B,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIrE,OAAO,CAAC,IAAI,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAOpD,SAAS,IAAI,MAAM;IASnB,QAAQ,IAAI,KAAK;IASjB,gBAAgB,IAAI,MAAM,EAAE;IAI5B,SAAS,IAAI,UAAU;IASjB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAqBhC;AAED,eAAO,MAAM,IAAI,SAAgB,CAAC"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../../src/sdk.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAIvC,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAIlC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,cAAM,OAAO;IACX,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,WAAW,CAAS;IAE5B,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IA2E9B,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIrE,OAAO,CAAC,IAAI,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAOpD,SAAS,IAAI,MAAM;IASnB,QAAQ,IAAI,KAAK;IASjB,gBAAgB,IAAI,MAAM,EAAE;IAI5B,SAAS,IAAI,UAAU;IAYvB,KAAK,IAAI,IAAI;IAWP,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAqBhC;AAED,eAAO,MAAM,IAAI,SAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-otel",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Lightweight OpenTelemetry SDK for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -36,7 +36,9 @@
36
36
  "clean": "del-cli lib",
37
37
  "prepare": "bob build",
38
38
  "typecheck": "tsc",
39
- "lint": "eslint \"**/*.{js,ts,tsx}\""
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "test": "jest",
41
+ "test:watch": "jest --watch"
40
42
  },
41
43
  "keywords": [
42
44
  "react-native",
@@ -66,15 +68,18 @@
66
68
  "@eslint/js": "^9.35.0",
67
69
  "@react-native/babel-preset": "0.83.0",
68
70
  "@react-native/eslint-config": "0.83.0",
71
+ "@types/jest": "^29",
69
72
  "@types/react": "^19.1.12",
70
73
  "del-cli": "^6.0.0",
71
74
  "eslint": "^9.35.0",
72
75
  "eslint-config-prettier": "^10.1.8",
73
76
  "eslint-plugin-prettier": "^5.5.4",
77
+ "jest": "^29",
74
78
  "prettier": "^2.8.8",
75
79
  "react": "19.2.0",
76
80
  "react-native": "0.83.2",
77
81
  "react-native-builder-bob": "^0.40.13",
82
+ "ts-jest": "^29",
78
83
  "turbo": "^2.5.6",
79
84
  "typescript": "^5.9.2"
80
85
  },
@@ -1,17 +1,70 @@
1
- import { Span, NoopSpan } from '../core/span';
1
+ import type { Span, NoopSpan } from '../core/span';
2
2
 
3
- // Module-level singleton. Tracks only the current active screen span.
4
- // Network spans do NOT touch this context (handled via activeNetworkSpans map).
5
- class SpanContextManager {
6
- private current_: Span | NoopSpan | undefined;
3
+ // Public interface what external consumers see when they import spanContext.
4
+ // Hides push/pop to prevent misuse; use tracer.startActiveSpan() or
5
+ // tracer.withSpan() for safe nested context management.
6
+ export interface SpanContextManagerPublic {
7
+ setCurrent(span: Span | NoopSpan | undefined): void;
8
+ current(): Span | NoopSpan | undefined;
9
+ }
10
+
11
+ // Each manual push is tracked by a unique token so that pop() can find the
12
+ // exact entry to remove regardless of concurrent async interleaving.
13
+ interface StackEntry {
14
+ span: Span | NoopSpan;
15
+ token: symbol;
16
+ }
7
17
 
18
+ class SpanContextManager implements SpanContextManagerPublic {
19
+ // Screen-level span set by navigation instrumentation.
20
+ private screenSpan_: Span | NoopSpan | undefined;
21
+ // Manual spans pushed by startActiveSpan / withSpan.
22
+ // Separate from screenSpan_ because they have different lifecycles.
23
+ private manualStack_: StackEntry[] = [];
24
+
25
+ // ─── Public API (backward-compatible) ──────────────────────────────────────
26
+
27
+ // Set the screen-level span. Called by navigation on route change.
28
+ // Clears the manual stack so stale sub-operation context from the previous
29
+ // screen does not leak into the new one.
8
30
  setCurrent(span: Span | NoopSpan | undefined): void {
9
- this.current_ = span;
31
+ this.screenSpan_ = span;
32
+ this.manualStack_ = [];
10
33
  }
11
34
 
35
+ // Return the most specific active span. Manual stack takes precedence.
12
36
  current(): Span | NoopSpan | undefined {
13
- return this.current_;
37
+ const top = this.manualStack_[this.manualStack_.length - 1];
38
+ return top?.span ?? this.screenSpan_;
39
+ }
40
+
41
+ // ─── Internal API (used by Tracer only, not re-exported from index.ts) ─────
42
+
43
+ // Push span as the active context. Returns a token required by pop().
44
+ /** @internal */
45
+ push(span: Span | NoopSpan): symbol {
46
+ const token = Symbol();
47
+ this.manualStack_.push({ span, token });
48
+ return token;
49
+ }
50
+
51
+ // Remove the entry matching token, regardless of position in the stack.
52
+ // Identity-based (not positional) so concurrent async operations cannot
53
+ // accidentally pop each other's entries.
54
+ /** @internal */
55
+ pop(token: symbol): void {
56
+ const idx = this.manualStack_.findIndex((e) => e.token === token);
57
+ if (idx !== -1) {
58
+ this.manualStack_.splice(idx, 1);
59
+ }
14
60
  }
15
61
  }
16
62
 
17
- export const spanContext = new SpanContextManager();
63
+ const manager = new SpanContextManager();
64
+
65
+ // External consumers get the narrowed type — push/pop are hidden.
66
+ export const spanContext: SpanContextManagerPublic = manager;
67
+
68
+ // Internal alias with the concrete type so tracer.ts can call push/pop.
69
+ // Not re-exported from index.ts.
70
+ export const spanContextInternal: SpanContextManager = manager;
package/src/core/ids.ts CHANGED
@@ -1,15 +1,29 @@
1
- function randomHex(length: number): string {
2
- let result = '';
3
- for (let i = 0; i < length; i++) {
4
- result += Math.floor(Math.random() * 16).toString(16);
1
+ function randomBytesHex(byteCount: number): string {
2
+ try {
3
+ const bytes = new Uint8Array(byteCount);
4
+ crypto.getRandomValues(bytes);
5
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
6
+ } catch {
7
+ // Fallback if crypto.getRandomValues is unavailable
8
+ if (__DEV__) {
9
+ console.warn(
10
+ '[react-native-otel] crypto.getRandomValues unavailable; falling back to Math.random for ID generation. IDs may collide at scale.'
11
+ );
12
+ }
13
+ let result = '';
14
+ for (let i = 0; i < byteCount * 2; i++) {
15
+ result += Math.floor(Math.random() * 16).toString(16);
16
+ }
17
+ return result;
5
18
  }
6
- return result;
7
19
  }
8
20
 
21
+ // 128-bit trace ID per OTel spec (32 hex chars)
9
22
  export function generateTraceId(): string {
10
- return randomHex(32);
23
+ return randomBytesHex(16);
11
24
  }
12
25
 
26
+ // 64-bit span ID per OTel spec (16 hex chars)
13
27
  export function generateSpanId(): string {
14
- return randomHex(16);
28
+ return randomBytesHex(8);
15
29
  }
package/src/core/meter.ts CHANGED
@@ -1,8 +1,17 @@
1
1
  import type { Attributes } from './attributes';
2
- import type { MetricExporter, MetricRecord } from '../exporters/types';
2
+ import type {
3
+ MetricExporter,
4
+ MetricRecord,
5
+ HistogramRecord,
6
+ } from '../exporters/types';
3
7
  import { sanitizeAttributes } from './attributes';
4
8
  import { now } from './clock';
5
9
 
10
+ // Default bucket boundaries in milliseconds — covers typical mobile latencies.
11
+ const DEFAULT_HISTOGRAM_BOUNDARIES = [
12
+ 0, 5, 10, 25, 50, 75, 100, 250, 500, 1000,
13
+ ];
14
+
6
15
  export class Counter {
7
16
  constructor(
8
17
  private name: string,
@@ -20,20 +29,92 @@ export class Counter {
20
29
  }
21
30
  }
22
31
 
32
+ interface HistogramBucket {
33
+ count: number;
34
+ sum: number;
35
+ bucketCounts: number[];
36
+ startTimeMs: number;
37
+ lastTimeMs: number;
38
+ attributes: Attributes;
39
+ }
40
+
41
+ export interface HistogramOptions {
42
+ // Explicit upper bounds for buckets, in ascending order. A +Inf bucket is
43
+ // always appended implicitly. Defaults to DEFAULT_HISTOGRAM_BOUNDARIES.
44
+ boundaries?: number[];
45
+ }
46
+
23
47
  export class Histogram {
48
+ private readonly boundaries: number[];
49
+ // Keyed by serialized attributes so concurrent recordings with different
50
+ // attribute sets are tracked independently.
51
+ private buckets = new Map<string, HistogramBucket>();
52
+
24
53
  constructor(
25
54
  private name: string,
26
- private pushToBuffer: (record: MetricRecord) => void
27
- ) {}
55
+ private pushToBuffer: (record: MetricRecord) => void,
56
+ options?: HistogramOptions
57
+ ) {
58
+ this.boundaries = options?.boundaries ?? DEFAULT_HISTOGRAM_BOUNDARIES;
59
+ }
28
60
 
29
61
  record(value: number, attrs?: Attributes): void {
30
- this.pushToBuffer({
31
- type: 'histogram',
32
- name: this.name,
33
- value,
34
- timestampMs: now(),
35
- attributes: attrs ? sanitizeAttributes(attrs) : {},
36
- });
62
+ const sanitized = attrs ? sanitizeAttributes(attrs) : {};
63
+ const key = JSON.stringify(sanitized);
64
+
65
+ let bucket = this.buckets.get(key);
66
+ if (!bucket) {
67
+ bucket = {
68
+ count: 0,
69
+ sum: 0,
70
+ bucketCounts: new Array<number>(this.boundaries.length + 1).fill(0),
71
+ startTimeMs: now(),
72
+ lastTimeMs: now(),
73
+ attributes: sanitized,
74
+ };
75
+ this.buckets.set(key, bucket);
76
+ }
77
+
78
+ bucket.count += 1;
79
+ bucket.sum += value;
80
+ bucket.lastTimeMs = now();
81
+
82
+ // Place value into its bucket (first boundary that the value is <= to).
83
+ let placed = false;
84
+ for (let i = 0; i < this.boundaries.length; i++) {
85
+ if (value <= this.boundaries[i]!) {
86
+ bucket.bucketCounts[i]! += 1;
87
+ placed = true;
88
+ break;
89
+ }
90
+ }
91
+ // +Inf bucket
92
+ if (!placed) {
93
+ bucket.bucketCounts[this.boundaries.length]! += 1;
94
+ }
95
+ }
96
+
97
+ // Called by Meter.flush() — drains accumulated buckets into the export buffer.
98
+ flush(): void {
99
+ for (const bucket of this.buckets.values()) {
100
+ const record: HistogramRecord = {
101
+ type: 'histogram',
102
+ name: this.name,
103
+ count: bucket.count,
104
+ sum: bucket.sum,
105
+ bucketBoundaries: this.boundaries,
106
+ bucketCounts: bucket.bucketCounts,
107
+ timestampMs: bucket.lastTimeMs,
108
+ attributes: bucket.attributes,
109
+ };
110
+ this.pushToBuffer(record);
111
+ }
112
+ this.buckets.clear();
113
+ }
114
+
115
+ // Returns whether there is any accumulated data.
116
+ hasData(): boolean {
117
+ return this.buckets.size > 0;
37
118
  }
38
119
  }
39
120
 
@@ -57,6 +138,7 @@ export class Gauge {
57
138
  export class Meter {
58
139
  private buffer: MetricRecord[] = [];
59
140
  private exporter: MetricExporter | undefined;
141
+ private histograms: Histogram[] = [];
60
142
 
61
143
  constructor(exporter?: MetricExporter) {
62
144
  this.exporter = exporter;
@@ -66,8 +148,10 @@ export class Meter {
66
148
  return new Counter(name, (r) => this.buffer.push(r));
67
149
  }
68
150
 
69
- createHistogram(name: string): Histogram {
70
- return new Histogram(name, (r) => this.buffer.push(r));
151
+ createHistogram(name: string, options?: HistogramOptions): Histogram {
152
+ const histogram = new Histogram(name, (r) => this.buffer.push(r), options);
153
+ this.histograms.push(histogram);
154
+ return histogram;
71
155
  }
72
156
 
73
157
  createGauge(name: string): Gauge {
@@ -75,6 +159,13 @@ export class Meter {
75
159
  }
76
160
 
77
161
  flush(): void {
162
+ // Drain all histogram buckets into the buffer first.
163
+ for (const histogram of this.histograms) {
164
+ if (histogram.hasData()) {
165
+ histogram.flush();
166
+ }
167
+ }
168
+
78
169
  if (this.buffer.length === 0) return;
79
170
  const toExport = this.buffer.splice(0, this.buffer.length);
80
171
  this.exporter?.export(toExport);
@@ -7,7 +7,15 @@ import {
7
7
  import type { Attributes } from './attributes';
8
8
  import type { SpanContext, SpanExporter, SpanKind } from './span';
9
9
  import { Span, NoopSpan } from './span';
10
- import { spanContext } from '../context/span-context';
10
+ import { spanContext, spanContextInternal } from '../context/span-context';
11
+
12
+ interface SpanOptions {
13
+ kind?: SpanKind;
14
+ attributes?: Attributes;
15
+ // Inherit traceId from this parent. Omit to use the current active span.
16
+ // Pass null to force a new root trace.
17
+ parent?: SpanContext | null;
18
+ }
11
19
 
12
20
  export class Tracer {
13
21
  private exporter: SpanExporter | undefined;
@@ -24,22 +32,14 @@ export class Tracer {
24
32
  this.getUserAttributes = params.getUserAttributes;
25
33
  }
26
34
 
27
- startSpan(
28
- name: string,
29
- options?: {
30
- kind?: SpanKind;
31
- attributes?: Attributes;
32
- // Pass the full parent context to inherit traceId.
33
- // If omitted, the current screen span is used automatically.
34
- // Pass null explicitly to force a new root trace.
35
- parent?: SpanContext | null;
36
- }
37
- ): Span | NoopSpan {
35
+ // Create a span without making it the active context.
36
+ // Use startActiveSpan() when you want sub-operations to auto-parent.
37
+ startSpan(name: string, options?: SpanOptions): Span | NoopSpan {
38
38
  if (this.sampleRate < 1.0 && Math.random() > this.sampleRate) {
39
39
  return new NoopSpan();
40
40
  }
41
41
 
42
- // Resolve parent: explicit > current screen span > none (new trace)
42
+ // Resolve parent: explicit > current active span > none (new root trace)
43
43
  const parent: SpanContext | undefined =
44
44
  options?.parent !== undefined
45
45
  ? options.parent ?? undefined
@@ -55,6 +55,92 @@ export class Tracer {
55
55
  });
56
56
  }
57
57
 
58
+ // Create a span, make it the active context for the duration of fn, then
59
+ // automatically end it. Sub-operations started inside fn via startSpan() will
60
+ // automatically parent to this span.
61
+ //
62
+ // For concurrent async work (multiple in-flight awaits), pass parent
63
+ // explicitly to startSpan() instead — the shared context stack is not safe
64
+ // for interleaved async operations.
65
+ startActiveSpan<T>(name: string, fn: (span: Span | NoopSpan) => T): T;
66
+ startActiveSpan<T>(
67
+ name: string,
68
+ options: SpanOptions,
69
+ fn: (span: Span | NoopSpan) => T
70
+ ): T;
71
+ startActiveSpan<T>(
72
+ name: string,
73
+ optionsOrFn: SpanOptions | ((span: Span | NoopSpan) => T),
74
+ fn?: (span: Span | NoopSpan) => T
75
+ ): T {
76
+ const options = typeof optionsOrFn === 'function' ? undefined : optionsOrFn;
77
+ const callback = typeof optionsOrFn === 'function' ? optionsOrFn : fn!;
78
+
79
+ const span = this.startSpan(name, options);
80
+ const token = spanContextInternal.push(span);
81
+
82
+ const cleanup = (isError: boolean, err?: unknown) => {
83
+ if (isError && err instanceof Error) {
84
+ span.setStatus('ERROR', err.message);
85
+ }
86
+ span.end();
87
+ spanContextInternal.pop(token);
88
+ };
89
+
90
+ try {
91
+ const result = callback(span);
92
+ if (result instanceof Promise) {
93
+ // Token-based pop fires after the promise settles, preserving identity
94
+ // even if other startActiveSpan calls interleave on the event loop.
95
+ return result.then(
96
+ (v) => {
97
+ cleanup(false);
98
+ return v;
99
+ },
100
+ (e: unknown) => {
101
+ cleanup(true, e);
102
+ throw e;
103
+ }
104
+ ) as T;
105
+ }
106
+ cleanup(false);
107
+ return result;
108
+ } catch (e) {
109
+ cleanup(true, e);
110
+ throw e;
111
+ }
112
+ }
113
+
114
+ // Make an existing span the active context for the duration of fn.
115
+ // Does NOT end the span — the caller owns its lifetime.
116
+ // Safe for synchronous work. For concurrent async work, see startActiveSpan.
117
+ withSpan<T>(span: Span | NoopSpan, fn: (span: Span | NoopSpan) => T): T {
118
+ const token = spanContextInternal.push(span);
119
+
120
+ const cleanup = () => spanContextInternal.pop(token);
121
+
122
+ try {
123
+ const result = fn(span);
124
+ if (result instanceof Promise) {
125
+ return result.then(
126
+ (v) => {
127
+ cleanup();
128
+ return v;
129
+ },
130
+ (e: unknown) => {
131
+ cleanup();
132
+ throw e;
133
+ }
134
+ ) as T;
135
+ }
136
+ cleanup();
137
+ return result;
138
+ } catch (e) {
139
+ cleanup();
140
+ throw e;
141
+ }
142
+ }
143
+
58
144
  recordEvent(name: string, attributes?: Attributes): void {
59
145
  spanContext.current()?.addEvent(name, attributes);
60
146
  }
@@ -4,6 +4,7 @@ import type {
4
4
  LogExporter,
5
5
  ReadonlySpan,
6
6
  MetricRecord,
7
+ HistogramRecord,
7
8
  LogRecord,
8
9
  } from './types';
9
10
 
@@ -60,10 +61,23 @@ export class ConsoleMetricExporter implements MetricExporter {
60
61
  if (!shouldLog(this.debug)) return;
61
62
 
62
63
  for (const metric of metrics) {
63
- console.log(
64
- `[OTEL METRIC] ${metric.name} ${metric.type} value=${metric.value}`,
65
- Object.keys(metric.attributes).length > 0 ? metric.attributes : ''
66
- );
64
+ if (metric.type === 'histogram') {
65
+ const h = metric as HistogramRecord;
66
+ const avg = h.count > 0 ? (h.sum / h.count).toFixed(2) : '0';
67
+ const bucketStr = h.bucketBoundaries
68
+ .map((b, i) => `≤${b}:${h.bucketCounts[i]}`)
69
+ .concat([`+Inf:${h.bucketCounts[h.bucketBoundaries.length]}`])
70
+ .join(' ');
71
+ console.log(
72
+ `[OTEL METRIC] ${h.name} histogram count=${h.count} sum=${h.sum} avg=${avg} [${bucketStr}]`,
73
+ Object.keys(h.attributes).length > 0 ? h.attributes : ''
74
+ );
75
+ } else {
76
+ console.log(
77
+ `[OTEL METRIC] ${metric.name} ${metric.type} value=${metric.value}`,
78
+ Object.keys(metric.attributes).length > 0 ? metric.attributes : ''
79
+ );
80
+ }
67
81
  }
68
82
  }
69
83
  }