@dash0/sdk-web 0.6.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.
Files changed (194) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +496 -0
  3. package/dist/dash0.iife.js +2 -0
  4. package/dist/dash0.iife.js.map +1 -0
  5. package/dist/dash0.js +2 -0
  6. package/dist/dash0.js.map +1 -0
  7. package/dist/dash0.umd.cjs +2 -0
  8. package/dist/dash0.umd.cjs.map +1 -0
  9. package/dist/modules/api/attributes.js +16 -0
  10. package/dist/modules/api/debug.js +5 -0
  11. package/dist/modules/api/events.js +30 -0
  12. package/dist/modules/api/identify.js +29 -0
  13. package/dist/modules/api/init.js +130 -0
  14. package/dist/modules/api/report-error.js +4 -0
  15. package/dist/modules/api/session.js +88 -0
  16. package/dist/modules/attributes/common.js +26 -0
  17. package/dist/modules/attributes/index.js +2 -0
  18. package/dist/modules/attributes/url.js +25 -0
  19. package/dist/modules/entrypoint/npm-package.js +12 -0
  20. package/dist/modules/entrypoint/script.js +60 -0
  21. package/dist/modules/instrumentations/errors/async-function-wrapping.js +105 -0
  22. package/dist/modules/instrumentations/errors/event-handlers.js +52 -0
  23. package/dist/modules/instrumentations/errors/index.js +10 -0
  24. package/dist/modules/instrumentations/errors/timers.js +46 -0
  25. package/dist/modules/instrumentations/errors/unhandled-error.js +137 -0
  26. package/dist/modules/instrumentations/errors/unhandled-promise-rejection.js +29 -0
  27. package/dist/modules/instrumentations/http/fetch.js +164 -0
  28. package/dist/modules/instrumentations/http/utils.js +35 -0
  29. package/dist/modules/instrumentations/navigation/event.js +44 -0
  30. package/dist/modules/instrumentations/navigation/index.js +6 -0
  31. package/dist/modules/instrumentations/navigation/page-load.js +83 -0
  32. package/dist/modules/instrumentations/navigation/page-transition.js +72 -0
  33. package/dist/modules/instrumentations/web-vitals.js +32 -0
  34. package/dist/modules/semantic-conventions.js +79 -0
  35. package/dist/modules/transport/batcher.js +48 -0
  36. package/dist/modules/transport/fetch.js +58 -0
  37. package/dist/modules/transport/index.js +66 -0
  38. package/dist/modules/utils/constants.js +2 -0
  39. package/dist/modules/utils/debug.js +19 -0
  40. package/dist/modules/utils/fn.js +3 -0
  41. package/dist/modules/utils/globals.js +36 -0
  42. package/dist/modules/utils/id.js +16 -0
  43. package/dist/modules/utils/id_test.js +33 -0
  44. package/dist/modules/utils/ignore-rules.js +48 -0
  45. package/dist/modules/utils/index.js +16 -0
  46. package/dist/modules/utils/listeners.js +18 -0
  47. package/dist/modules/utils/local-storage.js +19 -0
  48. package/dist/modules/utils/math.js +3 -0
  49. package/dist/modules/utils/obj.js +10 -0
  50. package/dist/modules/utils/on-last-chance.js +31 -0
  51. package/dist/modules/utils/origin.js +14 -0
  52. package/dist/modules/utils/otel/attributes.js +55 -0
  53. package/dist/modules/utils/otel/attributes_test.js +158 -0
  54. package/dist/modules/utils/otel/error.js +30 -0
  55. package/dist/modules/utils/otel/http.js +7 -0
  56. package/dist/modules/utils/otel/index.js +4 -0
  57. package/dist/modules/utils/otel/span.js +56 -0
  58. package/dist/modules/utils/otel/trace-context.js +60 -0
  59. package/dist/modules/utils/performance.js +162 -0
  60. package/dist/modules/utils/pick.js +17 -0
  61. package/dist/modules/utils/pick_test.js +104 -0
  62. package/dist/modules/utils/rate-limit.js +20 -0
  63. package/dist/modules/utils/session-storage.js +21 -0
  64. package/dist/modules/utils/tab-id.js +29 -0
  65. package/dist/modules/utils/time.js +28 -0
  66. package/dist/modules/utils/timers.js +62 -0
  67. package/dist/modules/utils/url.js +11 -0
  68. package/dist/modules/utils/wrap.js +22 -0
  69. package/dist/modules/vars.js +25 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -0
  71. package/dist/types/api/attributes.d.ts +12 -0
  72. package/dist/types/api/debug.d.ts +1 -0
  73. package/dist/types/api/events.d.ts +36 -0
  74. package/dist/types/api/identify.d.ts +9 -0
  75. package/dist/types/api/init.d.ts +31 -0
  76. package/dist/types/api/report-error.d.ts +2 -0
  77. package/dist/types/api/session.d.ts +3 -0
  78. package/dist/types/attributes/common.d.ts +6 -0
  79. package/dist/types/attributes/index.d.ts +2 -0
  80. package/dist/types/attributes/url.d.ts +2 -0
  81. package/dist/types/entrypoint/npm-package.d.ts +8 -0
  82. package/dist/types/entrypoint/script.d.ts +1 -0
  83. package/dist/types/instrumentations/errors/async-function-wrapping.d.ts +8 -0
  84. package/dist/types/instrumentations/errors/event-handlers.d.ts +1 -0
  85. package/dist/types/instrumentations/errors/index.d.ts +1 -0
  86. package/dist/types/instrumentations/errors/timers.d.ts +1 -0
  87. package/dist/types/instrumentations/errors/unhandled-error.d.ts +5 -0
  88. package/dist/types/instrumentations/errors/unhandled-promise-rejection.d.ts +2 -0
  89. package/dist/types/instrumentations/http/fetch.d.ts +1 -0
  90. package/dist/types/instrumentations/http/utils.d.ts +5 -0
  91. package/dist/types/instrumentations/navigation/event.d.ts +1 -0
  92. package/dist/types/instrumentations/navigation/index.d.ts +1 -0
  93. package/dist/types/instrumentations/navigation/page-load.d.ts +7 -0
  94. package/dist/types/instrumentations/navigation/page-transition.d.ts +7 -0
  95. package/dist/types/instrumentations/web-vitals.d.ts +1 -0
  96. package/dist/types/semantic-conventions.d.ts +68 -0
  97. package/dist/types/transport/batcher.d.ts +4 -0
  98. package/dist/types/transport/fetch.d.ts +1 -0
  99. package/dist/types/transport/index.d.ts +3 -0
  100. package/dist/types/utils/constants.d.ts +2 -0
  101. package/dist/types/utils/debug.d.ts +7 -0
  102. package/dist/types/utils/fn.d.ts +1 -0
  103. package/dist/types/utils/globals.d.ts +14 -0
  104. package/dist/types/utils/id.d.ts +6 -0
  105. package/dist/types/utils/id_test.d.ts +1 -0
  106. package/dist/types/utils/ignore-rules.d.ts +3 -0
  107. package/dist/types/utils/index.d.ts +16 -0
  108. package/dist/types/utils/listeners.d.ts +2 -0
  109. package/dist/types/utils/local-storage.d.ts +4 -0
  110. package/dist/types/utils/math.d.ts +1 -0
  111. package/dist/types/utils/obj.d.ts +5 -0
  112. package/dist/types/utils/on-last-chance.d.ts +5 -0
  113. package/dist/types/utils/origin.d.ts +1 -0
  114. package/dist/types/utils/otel/attributes.d.ts +9 -0
  115. package/dist/types/utils/otel/attributes_test.d.ts +1 -0
  116. package/dist/types/utils/otel/error.d.ts +24 -0
  117. package/dist/types/utils/otel/http.d.ts +2 -0
  118. package/dist/types/utils/otel/index.d.ts +4 -0
  119. package/dist/types/utils/otel/span.d.ts +10 -0
  120. package/dist/types/utils/otel/trace-context.d.ts +8 -0
  121. package/dist/types/utils/performance.d.ts +45 -0
  122. package/dist/types/utils/pick.d.ts +9 -0
  123. package/dist/types/utils/pick_test.d.ts +1 -0
  124. package/dist/types/utils/rate-limit.d.ts +5 -0
  125. package/dist/types/utils/session-storage.d.ts +4 -0
  126. package/dist/types/utils/tab-id.d.ts +8 -0
  127. package/dist/types/utils/time.d.ts +6 -0
  128. package/dist/types/utils/timers.d.ts +6 -0
  129. package/dist/types/utils/url.d.ts +6 -0
  130. package/dist/types/utils/wrap.d.ts +1 -0
  131. package/dist/types/vars.d.ts +123 -0
  132. package/package.json +124 -0
  133. package/src/api/attributes.ts +19 -0
  134. package/src/api/debug.ts +6 -0
  135. package/src/api/events.ts +67 -0
  136. package/src/api/identify.ts +43 -0
  137. package/src/api/init.ts +221 -0
  138. package/src/api/report-error.ts +6 -0
  139. package/src/api/session.ts +113 -0
  140. package/src/attributes/common.ts +42 -0
  141. package/src/attributes/index.ts +2 -0
  142. package/src/attributes/url.ts +25 -0
  143. package/src/entrypoint/npm-package.ts +14 -0
  144. package/src/entrypoint/script-tag.html +23 -0
  145. package/src/entrypoint/script.ts +82 -0
  146. package/src/instrumentations/errors/async-function-wrapping.ts +157 -0
  147. package/src/instrumentations/errors/event-handlers.ts +77 -0
  148. package/src/instrumentations/errors/index.ts +11 -0
  149. package/src/instrumentations/errors/timers.ts +52 -0
  150. package/src/instrumentations/errors/unhandled-error.ts +187 -0
  151. package/src/instrumentations/errors/unhandled-promise-rejection.ts +30 -0
  152. package/src/instrumentations/http/fetch.ts +194 -0
  153. package/src/instrumentations/http/utils.ts +49 -0
  154. package/src/instrumentations/navigation/event.ts +66 -0
  155. package/src/instrumentations/navigation/index.ts +7 -0
  156. package/src/instrumentations/navigation/page-load.ts +96 -0
  157. package/src/instrumentations/navigation/page-transition.ts +100 -0
  158. package/src/instrumentations/web-vitals.ts +37 -0
  159. package/src/semantic-conventions.ts +91 -0
  160. package/src/transport/batcher.ts +59 -0
  161. package/src/transport/fetch.ts +72 -0
  162. package/src/transport/index.ts +77 -0
  163. package/src/utils/constants.ts +3 -0
  164. package/src/utils/debug.ts +25 -0
  165. package/src/utils/fn.ts +3 -0
  166. package/src/utils/globals.ts +42 -0
  167. package/src/utils/id.ts +17 -0
  168. package/src/utils/id_test.ts +41 -0
  169. package/src/utils/ignore-rules.ts +59 -0
  170. package/src/utils/index.ts +16 -0
  171. package/src/utils/listeners.ts +18 -0
  172. package/src/utils/local-storage.ts +25 -0
  173. package/src/utils/math.ts +3 -0
  174. package/src/utils/obj.ts +12 -0
  175. package/src/utils/on-last-chance.ts +36 -0
  176. package/src/utils/origin.ts +15 -0
  177. package/src/utils/otel/attributes.ts +67 -0
  178. package/src/utils/otel/attributes_test.ts +181 -0
  179. package/src/utils/otel/error.ts +63 -0
  180. package/src/utils/otel/http.ts +9 -0
  181. package/src/utils/otel/index.ts +4 -0
  182. package/src/utils/otel/span.ts +70 -0
  183. package/src/utils/otel/trace-context.ts +84 -0
  184. package/src/utils/performance.ts +203 -0
  185. package/src/utils/pick.ts +19 -0
  186. package/src/utils/pick_test.ts +134 -0
  187. package/src/utils/rate-limit.ts +34 -0
  188. package/src/utils/session-storage.ts +27 -0
  189. package/src/utils/tab-id.ts +33 -0
  190. package/src/utils/time.ts +37 -0
  191. package/src/utils/timers.ts +79 -0
  192. package/src/utils/url.ts +12 -0
  193. package/src/utils/wrap.ts +33 -0
  194. package/src/vars.ts +169 -0
@@ -0,0 +1,59 @@
1
+ import { vars } from "../vars";
2
+
3
+ export function matchesAny(regexp: RegExp[], s: string) {
4
+ for (let i = 0, len = regexp.length; i < len; i++) {
5
+ if (regexp[i]!.test(s)) {
6
+ return true;
7
+ }
8
+ }
9
+
10
+ return false;
11
+ }
12
+
13
+ const DATA_URL_PREFIX = "data:";
14
+
15
+ export function isUrlIgnored(url?: string | number): boolean {
16
+ if (!url) {
17
+ return true;
18
+ }
19
+
20
+ // Force string conversion. During runtime we have seen that some URLs passed into this code path aren't actually
21
+ // strings. Reason currently unknown.
22
+ url = String(url);
23
+ if (!url) {
24
+ return true;
25
+ }
26
+
27
+ // We never want to track data URLs. Instead of matching these via regular expressions (which might be expensive),
28
+ // we are explicitly doing a startsWith ignore case check
29
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
30
+ if (url.substring == null || url.substring(0, DATA_URL_PREFIX.length).toLowerCase() === DATA_URL_PREFIX) {
31
+ return true;
32
+ }
33
+
34
+ // Disable monitoring of data transmission requests. The data transmission strategy already ensures
35
+ // that data transmission requests are not picked up internally. However, we have seen some users
36
+ // leverage custom (broken) XMLHttpRequest instrumentation to implement application code which
37
+ // then break the detection of data transmission requests.
38
+ if (isTransportRequest(url)) {
39
+ return true;
40
+ }
41
+
42
+ return matchesAny(vars.ignoreUrls, url);
43
+ }
44
+
45
+ function isTransportRequest(url: string) {
46
+ const lowerCaseUrl = url.toLowerCase();
47
+
48
+ for (let i = 0, len = vars.endpoints.length; i < len; i++) {
49
+ const endpoint = vars.endpoints[i]!;
50
+ if (lowerCaseUrl.startsWith(endpoint.url)) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
57
+ export function isErrorMessageIgnored(message?: string) {
58
+ return !message || matchesAny(vars.ignoreErrorMessages, message);
59
+ }
@@ -0,0 +1,16 @@
1
+ export * from "./debug";
2
+ export * from "./obj";
3
+ export * from "./fn";
4
+ export * from "./globals";
5
+ export * from "./id";
6
+ export * from "./listeners";
7
+ export * from "./local-storage";
8
+ export * from "./performance";
9
+ export * from "./rate-limit";
10
+ export * from "./time";
11
+ export * from "./constants";
12
+ export * from "./math";
13
+ export * from "./origin";
14
+ export * from "./url";
15
+ export * from "./pick";
16
+ export * from "./wrap";
@@ -0,0 +1,18 @@
1
+ // aliasing the global function for improved minification and
2
+ // protection against hasOwnProperty overrides.
3
+
4
+ export function addEventListener(target: EventTarget, eventType: string, callback: (arg: Event) => unknown) {
5
+ if (target.addEventListener) {
6
+ target.addEventListener(eventType, callback, false);
7
+ } else if ((target as any).attachEvent) {
8
+ (target as any).attachEvent("on" + eventType, callback);
9
+ }
10
+ }
11
+
12
+ export function removeEventListener(target: EventTarget, eventType: string, callback: () => unknown) {
13
+ if (target.removeEventListener) {
14
+ target.removeEventListener(eventType, callback, false);
15
+ } else if ((target as any).detachEvent) {
16
+ (target as any).detachEvent("on" + eventType, callback);
17
+ }
18
+ }
@@ -0,0 +1,25 @@
1
+ // localStorage API re-exposed to allow testing.
2
+
3
+ import { localStorage } from "./globals";
4
+
5
+ export const isSupported =
6
+ localStorage != null && typeof localStorage.getItem === "function" && typeof localStorage.setItem === "function";
7
+
8
+ export function getItem(k: string): string | null | undefined {
9
+ if (isSupported && localStorage) {
10
+ return localStorage.getItem(k);
11
+ }
12
+ return null;
13
+ }
14
+
15
+ export function setItem(k: string, v: string): void {
16
+ if (isSupported && localStorage) {
17
+ localStorage.setItem(k, v);
18
+ }
19
+ }
20
+
21
+ export function removeItem(k: string): void {
22
+ if (isSupported && localStorage) {
23
+ localStorage.removeItem(k);
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ export function roundToTwoDecimals(n: number) {
2
+ return Math.round(n * 100) / 100;
3
+ }
@@ -0,0 +1,12 @@
1
+ const globalHasOwnProperty = Object.prototype.hasOwnProperty;
2
+
3
+ /**
4
+ * protection against hasOwnProperty overrides.
5
+ */
6
+ export function hasOwnProperty(obj: Record<string, unknown>, key: string) {
7
+ return globalHasOwnProperty.call(obj, key);
8
+ }
9
+
10
+ export function hasKey<O extends object>(obj: O, key: string | number | symbol): key is keyof O {
11
+ return key in obj;
12
+ }
@@ -0,0 +1,36 @@
1
+ import { addEventListener } from "./listeners";
2
+ import { doc, win } from "./globals";
3
+
4
+ let isUnloading = false;
5
+
6
+ /**
7
+ * Triggers the `fn` when the HTML document is getting unloaded
8
+ * (or when it looks like it might be). Useful to flush batch activities.
9
+ */
10
+ export function onLastChance(fn: () => void) {
11
+ if (isUnloading) {
12
+ fn();
13
+ }
14
+
15
+ if (!doc || !win) return;
16
+
17
+ addEventListener(doc, "visibilitychange", function () {
18
+ if (doc!.visibilityState !== "visible") {
19
+ fn();
20
+ }
21
+ });
22
+
23
+ addEventListener(win, "pagehide", function () {
24
+ isUnloading = true;
25
+ fn();
26
+ });
27
+
28
+ // According to the spec visibilitychange should be a replacement for
29
+ // beforeunload, but the reality is different (as of 2019-04-17). Chrome will
30
+ // close tabs without firing visibilitychange. beforeunload on the other hand
31
+ // is fired.
32
+ addEventListener(win, "beforeunload", function () {
33
+ isUnloading = true;
34
+ fn();
35
+ });
36
+ }
@@ -0,0 +1,15 @@
1
+ import { parseUrl } from "./url";
2
+
3
+ /** Returns the origin if present (if in browser context). */
4
+ function getOrigin(): string | undefined {
5
+ return typeof location !== "undefined" ? location.origin : undefined;
6
+ }
7
+
8
+ export function isSameOrigin(url: string) {
9
+ try {
10
+ const parsedUrl = parseUrl(url);
11
+ return parsedUrl.origin === getOrigin();
12
+ } catch (_e) {
13
+ return false;
14
+ }
15
+ }
@@ -0,0 +1,67 @@
1
+ import { AnyValue, KeyValue } from "../../../types/otlp";
2
+
3
+ type PrimitiveAttributeValue = string | number | boolean;
4
+ export type AttributeValueType =
5
+ | PrimitiveAttributeValue
6
+ | Array<PrimitiveAttributeValue>
7
+ | Record<string, PrimitiveAttributeValue>;
8
+
9
+ const ANY_VALUE_KEYS = [
10
+ "stringValue",
11
+ "boolValue",
12
+ "intValue",
13
+ "doubleValue",
14
+ "arrayValue",
15
+ "kvlistValue",
16
+ "bytesValue",
17
+ ];
18
+
19
+ function isAnyValue(value: unknown): value is AnyValue {
20
+ if (value == null || typeof value !== "object") return false;
21
+
22
+ const keys = Object.keys(value);
23
+ return keys.length === 1 && ANY_VALUE_KEYS.includes(keys[0]!);
24
+ }
25
+
26
+ export function toAnyValue(value: AttributeValueType | AnyValue): AnyValue;
27
+ export function toAnyValue(value?: AttributeValueType | AnyValue): AnyValue | undefined;
28
+ export function toAnyValue(value?: AttributeValueType | AnyValue): AnyValue | undefined {
29
+ if (value == null) {
30
+ return undefined;
31
+ }
32
+
33
+ let anyValue: AnyValue = {};
34
+ if (Array.isArray(value)) {
35
+ anyValue["arrayValue"] = { values: value.map((e) => toAnyValue(e)) };
36
+ } else if (typeof value === "string") {
37
+ anyValue["stringValue"] = value;
38
+ } else if (typeof value === "number") {
39
+ anyValue["doubleValue"] = value;
40
+ } else if (typeof value === "boolean") {
41
+ anyValue["boolValue"] = value;
42
+ } else if (isAnyValue(value)) {
43
+ anyValue = value;
44
+ } else if (typeof value === "object") {
45
+ anyValue["kvlistValue"] = { values: Object.entries(value).map(([key, value]) => toKeyValue(key, value)) };
46
+ }
47
+
48
+ return anyValue;
49
+ }
50
+
51
+ export function toKeyValue(key: string, value: AttributeValueType | AnyValue): KeyValue {
52
+ return {
53
+ key: key,
54
+ value: toAnyValue(value),
55
+ };
56
+ }
57
+
58
+ export function addAttribute(attributes: KeyValue[], key: string, value: AttributeValueType | AnyValue) {
59
+ attributes.push(toKeyValue(key, value));
60
+ }
61
+
62
+ export function removeAttribute(attributes: KeyValue[], key: string) {
63
+ const index = attributes.findIndex((attr) => attr["key"] === key);
64
+ if (index !== -1) {
65
+ attributes.splice(index, 1);
66
+ }
67
+ }
@@ -0,0 +1,181 @@
1
+ import { expect, describe, it } from "vitest";
2
+ import { toAnyValue } from "./attributes";
3
+ import { AnyValue } from "../../../types/otlp";
4
+
5
+ describe("toAnyValue", () => {
6
+ describe("primitive values", () => {
7
+ it("converts string values to stringValue", () => {
8
+ const result = toAnyValue("hello");
9
+ expect(result).toEqual({ stringValue: "hello" });
10
+ });
11
+
12
+ it("converts number values to doubleValue", () => {
13
+ const result = toAnyValue(42);
14
+ expect(result).toEqual({ doubleValue: 42 });
15
+ });
16
+
17
+ it("converts boolean true to boolValue", () => {
18
+ const result = toAnyValue(true);
19
+ expect(result).toEqual({ boolValue: true });
20
+ });
21
+
22
+ it("converts boolean false to boolValue", () => {
23
+ const result = toAnyValue(false);
24
+ expect(result).toEqual({ boolValue: false });
25
+ });
26
+
27
+ it("converts zero to doubleValue", () => {
28
+ const result = toAnyValue(0);
29
+ expect(result).toEqual({ doubleValue: 0 });
30
+ });
31
+
32
+ it("converts negative numbers to doubleValue", () => {
33
+ const result = toAnyValue(-123.45);
34
+ expect(result).toEqual({ doubleValue: -123.45 });
35
+ });
36
+
37
+ it("converts empty string to stringValue", () => {
38
+ const result = toAnyValue("");
39
+ expect(result).toEqual({ stringValue: "" });
40
+ });
41
+ });
42
+
43
+ describe("array values", () => {
44
+ it("converts array of strings to arrayValue", () => {
45
+ const result = toAnyValue(["a", "b", "c"]);
46
+ expect(result).toEqual({
47
+ arrayValue: {
48
+ values: [{ stringValue: "a" }, { stringValue: "b" }, { stringValue: "c" }],
49
+ },
50
+ });
51
+ });
52
+
53
+ it("converts array of numbers to arrayValue", () => {
54
+ const result = toAnyValue([1, 2, 3]);
55
+ expect(result).toEqual({
56
+ arrayValue: {
57
+ values: [{ doubleValue: 1 }, { doubleValue: 2 }, { doubleValue: 3 }],
58
+ },
59
+ });
60
+ });
61
+
62
+ it("converts array of booleans to arrayValue", () => {
63
+ const result = toAnyValue([true, false]);
64
+ expect(result).toEqual({
65
+ arrayValue: {
66
+ values: [{ boolValue: true }, { boolValue: false }],
67
+ },
68
+ });
69
+ });
70
+
71
+ it("converts mixed array to arrayValue", () => {
72
+ const result = toAnyValue([1, "hello", true]);
73
+ expect(result).toEqual({
74
+ arrayValue: {
75
+ values: [{ doubleValue: 1 }, { stringValue: "hello" }, { boolValue: true }],
76
+ },
77
+ });
78
+ });
79
+
80
+ it("converts empty array to arrayValue", () => {
81
+ const result = toAnyValue([]);
82
+ expect(result).toEqual({
83
+ arrayValue: {
84
+ values: [],
85
+ },
86
+ });
87
+ });
88
+ });
89
+
90
+ describe("object values", () => {
91
+ it("converts simple object to kvlistValue", () => {
92
+ const result = toAnyValue({ name: "John", age: 30 });
93
+ expect(result).toEqual({
94
+ kvlistValue: {
95
+ values: [
96
+ { key: "name", value: { stringValue: "John" } },
97
+ { key: "age", value: { doubleValue: 30 } },
98
+ ],
99
+ },
100
+ });
101
+ });
102
+
103
+ it("converts empty object to kvlistValue", () => {
104
+ const result = toAnyValue({});
105
+ expect(result).toEqual({
106
+ kvlistValue: {
107
+ values: [],
108
+ },
109
+ });
110
+ });
111
+
112
+ it("converts object with mixed value types", () => {
113
+ const result = toAnyValue({
114
+ str: "text",
115
+ num: 42,
116
+ bool: true,
117
+ });
118
+ expect(result).toEqual({
119
+ kvlistValue: {
120
+ values: [
121
+ { key: "str", value: { stringValue: "text" } },
122
+ { key: "num", value: { doubleValue: 42 } },
123
+ { key: "bool", value: { boolValue: true } },
124
+ ],
125
+ },
126
+ });
127
+ });
128
+ });
129
+
130
+ describe("AnyValue passthrough", () => {
131
+ it("returns existing AnyValue unchanged", () => {
132
+ const anyValue: AnyValue = { stringValue: "test" };
133
+ const result = toAnyValue(anyValue);
134
+ expect(result).toBe(anyValue);
135
+ });
136
+
137
+ it("returns existing doubleValue AnyValue unchanged", () => {
138
+ const anyValue: AnyValue = { doubleValue: 123 };
139
+ const result = toAnyValue(anyValue);
140
+ expect(result).toBe(anyValue);
141
+ });
142
+
143
+ it("returns existing boolValue AnyValue unchanged", () => {
144
+ const anyValue: AnyValue = { boolValue: false };
145
+ const result = toAnyValue(anyValue);
146
+ expect(result).toBe(anyValue);
147
+ });
148
+
149
+ it("returns existing arrayValue AnyValue unchanged", () => {
150
+ const anyValue: AnyValue = {
151
+ arrayValue: {
152
+ values: [{ stringValue: "test" }],
153
+ },
154
+ };
155
+ const result = toAnyValue(anyValue);
156
+ expect(result).toBe(anyValue);
157
+ });
158
+
159
+ it("returns existing kvlistValue AnyValue unchanged", () => {
160
+ const anyValue: AnyValue = {
161
+ kvlistValue: {
162
+ values: [{ key: "test", value: { stringValue: "value" } }],
163
+ },
164
+ };
165
+ const result = toAnyValue(anyValue);
166
+ expect(result).toBe(anyValue);
167
+ });
168
+ });
169
+
170
+ describe("undefined and null handling", () => {
171
+ it("returns undefined for undefined", () => {
172
+ const result = toAnyValue(undefined);
173
+ expect(result).toBeUndefined();
174
+ });
175
+
176
+ it("returns undefined for null", () => {
177
+ const result = toAnyValue(null as any);
178
+ expect(result).toBeUndefined();
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,63 @@
1
+ import { addSpanEvent, InProgressSpan } from "./span";
2
+ import { KeyValue, SpanStatus } from "../../../types/otlp";
3
+ import { addAttribute } from "./attributes";
4
+ import {
5
+ SPAN_EVENT_NAME_EXCEPTION,
6
+ EXCEPTION_MESSAGE,
7
+ EXCEPTION_STACKTRACE,
8
+ EXCEPTION_TYPE,
9
+ SPAN_STATUS_ERROR,
10
+ } from "../../semantic-conventions";
11
+
12
+ interface ExceptionWithCode {
13
+ code: string | number;
14
+ name?: string;
15
+ message?: string;
16
+ stack?: string;
17
+ }
18
+
19
+ interface ExceptionWithMessage {
20
+ code?: string | number;
21
+ message: string;
22
+ name?: string;
23
+ stack?: string;
24
+ }
25
+
26
+ interface ExceptionWithName {
27
+ code?: string | number;
28
+ message?: string;
29
+ name: string;
30
+ stack?: string;
31
+ }
32
+
33
+ export type Exception = ExceptionWithCode | ExceptionWithMessage | ExceptionWithName | string;
34
+
35
+ export function recordException(span: InProgressSpan, exception: Exception) {
36
+ const attributes: KeyValue[] = [];
37
+
38
+ if (typeof exception === "string") {
39
+ addAttribute(attributes, EXCEPTION_MESSAGE, exception);
40
+ } else if (exception) {
41
+ if (exception.code) {
42
+ addAttribute(attributes, EXCEPTION_TYPE, exception.code.toString());
43
+ } else if (exception.name) {
44
+ addAttribute(attributes, EXCEPTION_TYPE, exception.name);
45
+ }
46
+
47
+ if (exception.message) {
48
+ addAttribute(attributes, EXCEPTION_MESSAGE, exception.message);
49
+ }
50
+ if (exception.stack) {
51
+ addAttribute(attributes, EXCEPTION_STACKTRACE, exception.stack);
52
+ }
53
+
54
+ addSpanEvent(span, SPAN_EVENT_NAME_EXCEPTION, attributes);
55
+ }
56
+ }
57
+
58
+ export function errorToSpanStatus(e: Exception): SpanStatus {
59
+ return {
60
+ code: SPAN_STATUS_ERROR,
61
+ message: e && typeof e === "object" && "message" in e ? (e.message as string) : String(e),
62
+ };
63
+ }
@@ -0,0 +1,9 @@
1
+ import { HTTP_REQUEST_HEADER, HTTP_RESPONSE_HEADER } from "../../semantic-conventions";
2
+
3
+ export function httpRequestHeaderKey(key: string) {
4
+ return `${HTTP_REQUEST_HEADER}.${key.toLowerCase()}`;
5
+ }
6
+
7
+ export function httpResponseHeaderKey(key: string) {
8
+ return `${HTTP_RESPONSE_HEADER}.${key.toLowerCase()}`;
9
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./attributes";
2
+ export * from "./trace-context";
3
+ export * from "./span";
4
+ export * from "./error";
@@ -0,0 +1,70 @@
1
+ import { KeyValue, Span, SpanStatus } from "../../../types/otlp";
2
+ import { nowNanos } from "../time";
3
+ import { generateUniqueId, SPAN_ID_BYTES, TRACE_ID_BYTES } from "../id";
4
+ import { SPAN_KIND_CLIENT, SPAN_STATUS_UNSET } from "../../semantic-conventions";
5
+
6
+ export type InProgressSpan = Omit<Span, "endTimeUnixNano">;
7
+
8
+ export function startSpan(name: string): InProgressSpan {
9
+ return {
10
+ traceId: generateUniqueId(TRACE_ID_BYTES),
11
+ spanId: generateUniqueId(SPAN_ID_BYTES),
12
+ name,
13
+ // Always CLIENT for now https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/trace/v1/trace.proto#L143-L169
14
+ // Note: we directly define otlp here, this differs from the values used by oteljs internally.
15
+ kind: SPAN_KIND_CLIENT,
16
+ startTimeUnixNano: nowNanos(),
17
+ attributes: [],
18
+ events: [],
19
+ links: [],
20
+ status: { code: SPAN_STATUS_UNSET },
21
+ };
22
+ }
23
+
24
+ export function endSpan(span: InProgressSpan, status: SpanStatus | undefined, durationNano: number | undefined): Span {
25
+ // We cast here to avoid having to instantiate a copy of the span
26
+ const s = span as Span;
27
+ if (status) {
28
+ s.status = status;
29
+ }
30
+ s.endTimeUnixNano =
31
+ durationNano != null ? String(Math.round(parseInt(s.startTimeUnixNano) + durationNano)) : nowNanos();
32
+ return s;
33
+ }
34
+
35
+ /**
36
+ * Adds an event to a span. Can optionally accept the events timestamp and attributes for the event.
37
+ * The timestamp needs to be specified as nanoseconds since unix epoch in string format.
38
+ */
39
+ export function addSpanEvent(
40
+ span: InProgressSpan,
41
+ name: string,
42
+ attributesOrTs?: string | KeyValue[] | undefined,
43
+ attributes?: KeyValue[] | undefined
44
+ ) {
45
+ let ts: string | undefined = undefined;
46
+ let attr: KeyValue[] | undefined = undefined;
47
+
48
+ if (typeof attributesOrTs === "string") {
49
+ ts = attributesOrTs;
50
+ } else if (Array.isArray(attributesOrTs)) {
51
+ attr = attributesOrTs;
52
+ }
53
+
54
+ if (attributes) {
55
+ attr = attributes;
56
+ }
57
+
58
+ span.events.push({
59
+ name,
60
+ timeUnixNano: ts != null ? ts : nowNanos(),
61
+ attributes: attr ?? [],
62
+ });
63
+ }
64
+
65
+ export function setSpanStatus(span: InProgressSpan, code: SpanStatus["code"], message?: string) {
66
+ span.status = {
67
+ code,
68
+ message,
69
+ };
70
+ }
@@ -0,0 +1,84 @@
1
+ import { doc } from "../globals";
2
+ import { perf } from "../globals";
3
+ import { InProgressSpan } from "./span";
4
+
5
+ const TRACE_PARENT_HEADER = "traceparent";
6
+
7
+ type TraceContext = {
8
+ traceId?: string;
9
+ spanId?: string;
10
+ };
11
+
12
+ /**
13
+ * See https://www.w3.org/TR/trace-context/#traceparent-header
14
+ */
15
+ const w3cTraceparentFormat = /^00-([a-f0-9]{32})-([a-f0-9]{16})-[0-9]{1,2}$/;
16
+
17
+ export function getTraceContextForPageLoad(): TraceContext | undefined {
18
+ const match =
19
+ getTraceparentFromMetaElement().match(w3cTraceparentFormat) ||
20
+ getTraceparentFromNavigationTiming().match(w3cTraceparentFormat);
21
+ if (match) {
22
+ return {
23
+ traceId: match[1],
24
+ spanId: match[2],
25
+ };
26
+ }
27
+
28
+ return undefined;
29
+ }
30
+
31
+ function getTraceparentFromMetaElement(): string {
32
+ return (
33
+ Array.from(doc?.getElementsByTagName("meta") ?? [])
34
+ .find((e) => e.getAttribute("name")?.toLowerCase() === TRACE_PARENT_HEADER)
35
+ ?.content.trim() || ""
36
+ );
37
+ }
38
+
39
+ function getTraceparentFromNavigationTiming(): string {
40
+ const nt = perf.getEntriesByType("navigation")[0];
41
+ if (!nt) {
42
+ return "";
43
+ }
44
+
45
+ if (!nt.serverTiming) {
46
+ return "";
47
+ }
48
+
49
+ return getTraceparentFromServerTiming(nt.serverTiming);
50
+ }
51
+
52
+ function getTraceparentFromServerTiming(serverTimings: readonly PerformanceServerTiming[]): string {
53
+ for (const serverEntry of serverTimings) {
54
+ if (serverEntry.name === TRACE_PARENT_HEADER) {
55
+ return serverEntry.description.trim();
56
+ }
57
+ }
58
+
59
+ return "";
60
+ }
61
+
62
+ export function addTraceContextHttpHeaders(
63
+ fn: (name: string, value: string) => void,
64
+ ctx: unknown,
65
+ span: InProgressSpan
66
+ ) {
67
+ /**
68
+ * W3C Traceparent header.
69
+ * General format is ${version}-${trace-id}-${parent-id}-${trace-flags}
70
+ *
71
+ * The only spec'd version is currently 00
72
+ * Trace flags are an 8 bit field of bit flags:
73
+ * Sampled: 00000001 - Should only be set if a definite decision to record the trace was made.
74
+ * If set downstream processors should also record the trace
75
+ * Random Trace ID: 00000010 - IF set the component guarantees that the seven right most bytes of the trace-id
76
+ * are randomly generated. Downstream processors are then able to rely on this for technical things like shard keys.
77
+ *
78
+ * References:
79
+ * https://www.w3.org/TR/trace-context-2/#traceparent-header
80
+ * https://www.w3.org/TR/trace-context-2/#trace-flags
81
+ * https://www.w3.org/TR/trace-context-2/#random-trace-id-flag
82
+ */
83
+ fn.call(ctx, "traceparent", `00-${span.traceId}-${span.spanId}-03`);
84
+ }