@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,96 @@
1
+ import { addEventListener, debug, doc, win, roundToTwoDecimals, error, toNanosTs, getTimeOrigin } from "../../utils";
2
+ import { KeyValue, LogRecord } from "../../../types/otlp";
3
+ import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../../semantic-conventions";
4
+ import { sendLog } from "../../transport";
5
+ import { getTraceContextForPageLoad, addAttribute } from "../../utils/otel";
6
+ import { addCommonAttributes } from "../../attributes";
7
+ import { transmitPageViewEvent } from "./event";
8
+
9
+ /**
10
+ * Tracks page loads as per this OTel spec:
11
+ * https://github.com/open-telemetry/semantic-conventions/pull/1910/files
12
+ *
13
+ * Notable difference: The full URL is transmitted as a signal attribute.
14
+ */
15
+ export function startPageLoadInstrumentation() {
16
+ try {
17
+ transmitPageViewEvent(getStartTimeUnixNanos(), win?.location.href ? new URL(win?.location.href) : undefined);
18
+ } catch (e) {
19
+ error("Failed to transmit initial page view event", e);
20
+ }
21
+
22
+ if (doc?.readyState === "complete") {
23
+ return onLoaded();
24
+ }
25
+
26
+ if (win) {
27
+ addEventListener(win, "load", function () {
28
+ // we want to get timing data for loadEventEnd,
29
+ // so asynchronously process this
30
+ setTimeout(onLoaded, 0);
31
+ });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * See https://github.com/open-telemetry/semantic-conventions/pull/1919
37
+ */
38
+ function onLoaded() {
39
+ const nt = win?.performance.getEntriesByType("navigation")[0];
40
+ if (!nt) {
41
+ debug("Navigation timings not available. Cannot emit navigation timing log");
42
+ return;
43
+ }
44
+
45
+ const attributes: KeyValue[] = [];
46
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.NAVIGATION_TIMING);
47
+
48
+ const bodyAttributes: KeyValue[] = [];
49
+ addAttribute(bodyAttributes, "name", nt.name);
50
+ addNavigationTiming(bodyAttributes, nt, "responseStatus");
51
+
52
+ addNavigationTiming(bodyAttributes, nt, "fetchStart");
53
+ addNavigationTiming(bodyAttributes, nt, "requestStart");
54
+ addNavigationTiming(bodyAttributes, nt, "responseStart");
55
+ addNavigationTiming(bodyAttributes, nt, "domInteractive");
56
+ addNavigationTiming(bodyAttributes, nt, "domContentLoadedEventEnd");
57
+ addNavigationTiming(bodyAttributes, nt, "domComplete");
58
+ addNavigationTiming(bodyAttributes, nt, "loadEventEnd");
59
+
60
+ addNavigationTiming(bodyAttributes, nt, "transferSize");
61
+ addNavigationTiming(bodyAttributes, nt, "encodedBodySize");
62
+ addNavigationTiming(bodyAttributes, nt, "decodedBodySize");
63
+
64
+ const log: LogRecord = {
65
+ timeUnixNano: getStartTimeUnixNanos(),
66
+ attributes: attributes,
67
+ severityNumber: LOG_SEVERITIES.INFO,
68
+ severityText: "INFO",
69
+ body: {
70
+ kvlistValue: {
71
+ values: bodyAttributes,
72
+ },
73
+ },
74
+ };
75
+ addCommonAttributes(log.attributes);
76
+
77
+ const traceContext = getTraceContextForPageLoad();
78
+ if (traceContext) {
79
+ log.traceId = traceContext.traceId;
80
+ log.spanId = traceContext.spanId;
81
+ }
82
+
83
+ sendLog(log);
84
+ }
85
+
86
+ function addNavigationTiming(attributes: KeyValue[], nt: PerformanceNavigationTiming, field: string) {
87
+ // @ts-expect-error index access not recognized by TS, but this makes the code more reusable
88
+ const value = nt[field];
89
+ if (typeof value === "number" && !isNaN(value)) {
90
+ addAttribute(attributes, field, Number.isInteger(value) ? value : roundToTwoDecimals(value));
91
+ }
92
+ }
93
+
94
+ function getStartTimeUnixNanos(): string {
95
+ return toNanosTs(Math.round(getTimeOrigin()));
96
+ }
@@ -0,0 +1,100 @@
1
+ import { debug, nowNanos, win, wrap } from "../../utils";
2
+ import { vars } from "../../vars";
3
+ import { transmitPageViewEvent } from "./event";
4
+
5
+ type LocationState = Partial<{
6
+ path: string;
7
+ search: string;
8
+ hash: string;
9
+ }>;
10
+
11
+ let currentLocation: LocationState = {};
12
+ let shouldIncludeHashChanges = false;
13
+ let shouldIncludeSearchChanges = false;
14
+
15
+ /**
16
+ * Tracks page transitions (virtual page views) as per this OTel spec:
17
+ * https://github.com/open-telemetry/semantic-conventions/pull/1910/files
18
+ *
19
+ * Notable difference: The full URL is transmitted as a signal attribute.
20
+ */
21
+ export function startPageTransitionInstrumentation() {
22
+ if (!win || !win.history) {
23
+ debug("Browser does not support history API, skipping instrumentation");
24
+ return;
25
+ }
26
+
27
+ if (!vars.pageViewInstrumentation.trackVirtualPageViews) {
28
+ return;
29
+ }
30
+
31
+ shouldIncludeSearchChanges = vars.pageViewInstrumentation.includeParts?.includes("SEARCH") ?? false;
32
+ shouldIncludeHashChanges = vars.pageViewInstrumentation.includeParts?.includes("HASH") ?? false;
33
+
34
+ /**
35
+ * Not wrapping history.go, history.backward and history.forward here, because their call signatures don't receive
36
+ * url information. For these cases we can use the popstate and hashchange events.
37
+ */
38
+ wrap(
39
+ win.history,
40
+ "replaceState",
41
+ (original) =>
42
+ function (this: History, state, unused, url) {
43
+ onUrlChange(url ? String(url) : undefined, true);
44
+ return original.apply(this, [state, unused, url]);
45
+ }
46
+ );
47
+ wrap(
48
+ win.history,
49
+ "pushState",
50
+ (original) =>
51
+ function (this: History, state, unused, url) {
52
+ onUrlChange(url ? String(url) : undefined);
53
+ return original.apply(this, [state, unused, url]);
54
+ }
55
+ );
56
+
57
+ win.addEventListener("hashchange", onHashChange);
58
+ win.addEventListener("popstate", onPopState);
59
+
60
+ try {
61
+ updateCurrentLocation(new URL(win.location.href));
62
+ } catch {}
63
+ }
64
+
65
+ function onPopState() {
66
+ // popState event is fired after the location entry has been updated, so the location should already contain the new url.
67
+ onUrlChange(win?.location.href);
68
+ }
69
+
70
+ function onHashChange(event: HashChangeEvent) {
71
+ onUrlChange(event.newURL);
72
+ }
73
+
74
+ function onUrlChange(url?: string, replaced?: boolean) {
75
+ if (!url) return;
76
+
77
+ try {
78
+ const parsedUrl = new URL(url, win?.location.href);
79
+ if (isLocationChange(parsedUrl)) {
80
+ updateCurrentLocation(parsedUrl);
81
+ transmitPageViewEvent(nowNanos(), parsedUrl, true, Boolean(replaced));
82
+ }
83
+ } catch (e) {
84
+ debug("Failed to handle url change", e);
85
+ }
86
+ }
87
+
88
+ function updateCurrentLocation(url: URL) {
89
+ currentLocation.path = url.pathname;
90
+ currentLocation.search = url.search;
91
+ currentLocation.hash = url.hash;
92
+ }
93
+
94
+ function isLocationChange(newUrl: URL) {
95
+ return (
96
+ newUrl.pathname !== currentLocation.path ||
97
+ (shouldIncludeSearchChanges && newUrl.search !== currentLocation.search) ||
98
+ (shouldIncludeHashChanges && newUrl.hash !== currentLocation.hash)
99
+ );
100
+ }
@@ -0,0 +1,37 @@
1
+ import { onLCP, onINP, onCLS, Metric } from "web-vitals";
2
+ import { KeyValue, LogRecord } from "../../types/otlp";
3
+ import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../semantic-conventions";
4
+ import { nowNanos, roundToTwoDecimals } from "../utils";
5
+ import { sendLog } from "../transport";
6
+ import { addAttribute } from "../utils/otel";
7
+ import { addCommonAttributes } from "../attributes";
8
+
9
+ export function startWebVitalsInstrumentation() {
10
+ onLCP(onWebVital);
11
+ onINP(onWebVital);
12
+ onCLS(onWebVital);
13
+ }
14
+
15
+ function onWebVital(metric: Metric) {
16
+ const attributes: KeyValue[] = [];
17
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.WEB_VITAL);
18
+
19
+ const bodyAttributes: KeyValue[] = [];
20
+ addAttribute(bodyAttributes, "name", metric.name);
21
+ addAttribute(bodyAttributes, "value", roundToTwoDecimals(metric.value));
22
+ addAttribute(bodyAttributes, "delta", roundToTwoDecimals(metric.delta));
23
+
24
+ const log: LogRecord = {
25
+ timeUnixNano: nowNanos(),
26
+ attributes: attributes,
27
+ severityNumber: LOG_SEVERITIES.INFO,
28
+ severityText: "INFO",
29
+ body: {
30
+ kvlistValue: {
31
+ values: bodyAttributes,
32
+ },
33
+ },
34
+ };
35
+ addCommonAttributes(log.attributes);
36
+ sendLog(log);
37
+ }
@@ -0,0 +1,91 @@
1
+ // Resource Attribute Keys
2
+ export const SERVICE_NAME = "service.name";
3
+ export const SERVICE_VERSION = "service.version";
4
+ export const DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
5
+ export const DEPLOYMENT_NAME = "deployment.name";
6
+ export const DEPLOYMENT_ID = "deployment.id";
7
+
8
+ // Misc Signal Attribute Keys
9
+ export const EVENT_NAME = "event.name";
10
+ export const EVENT_TITLE = "dash0.event.title";
11
+ export const PAGE_LOAD_ID = "page.load.id";
12
+ export const SESSION_ID = "session.id";
13
+ export const USER_AGENT = "user_agent.original";
14
+ export const BROWSER_TAB_ID = "browser.tab.id";
15
+ export const WINDOW_WIDTH = "browser.window.width";
16
+ export const WINDOW_HEIGHT = "browser.window.height";
17
+ export const NETWORK_CONNECTION_TYPE = "network.connection.subtype";
18
+ export const EXCEPTION_COMPONENT_STACK = "exception.component_stack";
19
+
20
+ // User Attribute Keys
21
+ export const USER_ID = "user.id";
22
+ export const USER_NAME = "user.name";
23
+ export const USER_FULL_NAME = "user.full_name";
24
+ export const USER_EMAIL = "user.email";
25
+ export const USER_HASH = "user.hash";
26
+ export const USER_ROLES = "user.roles";
27
+
28
+ // Exception Attribute Keys
29
+ export const EXCEPTION_MESSAGE = "exception.message";
30
+ export const EXCEPTION_TYPE = "exception.type";
31
+ export const EXCEPTION_STACKTRACE = "exception.stacktrace";
32
+
33
+ // URL Attribute Keys
34
+ export const URL_DOMAIN = "url.domain";
35
+ export const URL_FRAGMENT = "url.fragment";
36
+ export const URL_FULL = "url.full";
37
+ export const URL_PATH = "url.path";
38
+ export const URL_QUERY = "url.query";
39
+ export const URL_SCHEME = "url.scheme";
40
+
41
+ // Http Attribute Keys
42
+ export const HTTP_REQUEST_METHOD = "http.request.method";
43
+ export const HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original";
44
+ export const HTTP_REQUEST_HEADER = "http.request.header";
45
+ export const HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
46
+ export const HTTP_RESPONSE_HEADER = "http.response.header";
47
+ export const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
48
+
49
+ // Event Names
50
+ export const EVENT_NAMES = {
51
+ PAGE_VIEW: "browser.page_view",
52
+ NAVIGATION_TIMING: "browser.navigation_timing",
53
+ WEB_VITAL: "browser.web_vital",
54
+ ERROR: "browser.error",
55
+ };
56
+ export const SPAN_EVENT_NAME_EXCEPTION = "exception";
57
+
58
+ // Log Severities
59
+ export const LOG_SEVERITIES = {
60
+ UNSPECIFIED: 0,
61
+ TRACE: 1,
62
+ DEBUG: 5,
63
+ INFO: 9,
64
+ WARN: 13,
65
+ ERROR: 17,
66
+ FATAL: 21,
67
+ };
68
+ export type LOG_SEVERITY_TEXT = keyof typeof LOG_SEVERITIES;
69
+ export type LOG_SEVERITY_NUMBER = (typeof LOG_SEVERITIES)[LOG_SEVERITY_TEXT];
70
+
71
+ // Page View Event Attributes
72
+ // SEE: https://github.com/open-telemetry/semantic-conventions/pull/1910/files
73
+ export const PAGE_VIEW_TYPE = "type";
74
+ export const PAGE_VIEW_TYPE_VALUES = {
75
+ INITIAL: 0,
76
+ VIRTUAL: 1,
77
+ };
78
+ export const PAGE_VIEW_CHANGE_STATE = "change_state";
79
+ export const PAGE_VIEW_CHANGE_STATE_VALUES = {
80
+ PUSH: "pushState",
81
+ REPLACE: "replaceState",
82
+ };
83
+
84
+ // Span Status
85
+ export const SPAN_STATUS_UNSET = 0;
86
+ export const SPAN_STATUS_OK = 1; // This is here for completion, status ok is reserved for use by application developers
87
+ export const SPAN_STATUS_ERROR = 2;
88
+
89
+ // Span Kind
90
+ // See: https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/trace/v1/trace.proto#L143-L169
91
+ export const SPAN_KIND_CLIENT = 3;
@@ -0,0 +1,59 @@
1
+ import { onLastChance } from "../utils/on-last-chance";
2
+ import { doc } from "../utils";
3
+ import { setTimeout } from "../utils/timers";
4
+
5
+ const SCHEDULE_DELAY_MILLIS = 1000;
6
+ const MAX_QUEUE_SIZE = 15;
7
+
8
+ export type Batcher<T> = {
9
+ send(item: T): void;
10
+ };
11
+
12
+ export function newBatcher<T>(sendInternal: (items: T[]) => void): Batcher<T> {
13
+ const queuedItems: T[] = [];
14
+ let pendingFlushTimeout: ReturnType<typeof setTimeout> | null;
15
+
16
+ // We attempt batching of messages to be more efficient on the client, network and
17
+ // server-side. While the connection is either a persistent HTTP 2 connection or
18
+ // an HTTP 1.1 connection with keep-alive, there is still some overhead involved
19
+ // in having many small messages.
20
+ //
21
+ // For this reason we attempt batching. When batching we must be careful to
22
+ // force a transmission when the document is unloaded.
23
+ onLastChance(flush);
24
+
25
+ return {
26
+ send(item: T): void {
27
+ if (isWindowHidden()) {
28
+ // We cannot guarantee that we will ever get time to transmit data in a batched
29
+ // format when the window is hidden, as this might occur while the document is
30
+ // being unloaded. Immediately force a transmission in these cases.
31
+ sendInternal([item]);
32
+ return;
33
+ }
34
+
35
+ queuedItems.push(item);
36
+ if (queuedItems.length >= MAX_QUEUE_SIZE) {
37
+ flush();
38
+ } else if (pendingFlushTimeout == null) {
39
+ pendingFlushTimeout = setTimeout(flush, SCHEDULE_DELAY_MILLIS);
40
+ }
41
+ },
42
+ };
43
+
44
+ function flush() {
45
+ if (pendingFlushTimeout != null) {
46
+ clearTimeout(pendingFlushTimeout);
47
+ pendingFlushTimeout = null;
48
+ }
49
+
50
+ if (queuedItems.length > 0) {
51
+ sendInternal(queuedItems.slice());
52
+ queuedItems.length = 0;
53
+ }
54
+ }
55
+ }
56
+
57
+ function isWindowHidden() {
58
+ return doc?.visibilityState !== "visible";
59
+ }
@@ -0,0 +1,72 @@
1
+ import { vars } from "../vars";
2
+ import { noop, warn, fetch } from "../utils";
3
+
4
+ const BEACON_BODY_SIZE_LIMIT = 60000;
5
+
6
+ export async function send(path: string, body: unknown): Promise<void> {
7
+ const jsonString = JSON.stringify(body);
8
+ let requestBody: ArrayBuffer | string = jsonString;
9
+ let byteLength = jsonString.length;
10
+ let isCompressed = false;
11
+
12
+ // Try to compress if supported
13
+ if (typeof CompressionStream !== "undefined") {
14
+ requestBody = await compressWithGzip(jsonString);
15
+ byteLength = requestBody.byteLength;
16
+ isCompressed = true;
17
+ }
18
+
19
+ // Send to all endpoints in parallel
20
+ await Promise.all(
21
+ vars.endpoints.map(async (endpoint) => {
22
+ try {
23
+ const url = new URL(endpoint["url"]);
24
+ url.pathname = url.pathname + (url.pathname.endsWith("/") ? path.substring(1) : path);
25
+
26
+ const headers: HeadersInit = {
27
+ "Content-Type": "application/json",
28
+ Authorization: `Bearer ${endpoint["authToken"]}`,
29
+ };
30
+
31
+ if (endpoint.dataset) {
32
+ headers["Dash0-Dataset"] = endpoint["dataset"];
33
+ }
34
+
35
+ // Try to compress if supported
36
+ if (isCompressed) {
37
+ headers["Content-Encoding"] = "gzip";
38
+ }
39
+
40
+ if (!fetch) {
41
+ warn("Unable to send telemetry, fetch is not defined");
42
+ return;
43
+ }
44
+
45
+ const response = await fetch(url, {
46
+ method: "POST",
47
+ headers,
48
+ body: requestBody,
49
+
50
+ // The keepalive flag is related to the window.sendBeacon API. This in turn has size limitations.
51
+ keepalive: byteLength <= BEACON_BODY_SIZE_LIMIT,
52
+ });
53
+
54
+ // read the body so the connection can be closed
55
+ response.text().catch(noop);
56
+
57
+ if (!response.ok) {
58
+ warn(`Failed to send telemetry to ${url}: ${response.status} ${response.statusText}`);
59
+ }
60
+ } catch (error) {
61
+ warn(`Error sending telemetry to ${endpoint.url}${path}:`, error);
62
+ }
63
+ })
64
+ );
65
+ }
66
+
67
+ async function compressWithGzip(data: string): Promise<ArrayBuffer> {
68
+ const blob = new Blob([data]);
69
+ const stream = blob.stream();
70
+ const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
71
+ return new Response(compressedStream).arrayBuffer();
72
+ }
@@ -0,0 +1,77 @@
1
+ import { ExportLogsServiceRequest, ExportTraceServiceRequest, LogRecord, Span } from "../../types/otlp";
2
+ import { newBatcher } from "./batcher";
3
+ import { send } from "./fetch";
4
+ import { vars } from "../vars";
5
+ import { createRateLimiter } from "../utils/rate-limit";
6
+ import { debug, error } from "../utils";
7
+
8
+ const logBatcher = newBatcher<LogRecord>(sendLogs);
9
+ const spanBatcher = newBatcher<Span>(sendSpans);
10
+
11
+ let rateLimiter: (() => boolean) | undefined;
12
+
13
+ function isRateLimited() {
14
+ if (!rateLimiter) {
15
+ rateLimiter = createRateLimiter({
16
+ maxCalls: 8096,
17
+ maxCallsPerTenMinutes: 4096,
18
+ maxCallsPerTenSeconds: 128,
19
+ });
20
+ }
21
+
22
+ return rateLimiter();
23
+ }
24
+
25
+ export function sendLog(log: LogRecord): void {
26
+ if (isRateLimited()) {
27
+ debug("Transport rate limit. Will not send item.", log);
28
+ return;
29
+ }
30
+
31
+ logBatcher.send(log);
32
+ }
33
+
34
+ function sendLogs(logs: LogRecord[]): void {
35
+ send("/v1/logs", {
36
+ resourceLogs: [
37
+ {
38
+ resource: vars.resource,
39
+ scopeLogs: [
40
+ {
41
+ scope: vars.scope,
42
+ logRecords: logs,
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ } satisfies ExportLogsServiceRequest).catch((err) => {
48
+ error("Failed to transmit logs", err);
49
+ });
50
+ }
51
+
52
+ export function sendSpan(span: Span): void {
53
+ if (isRateLimited()) {
54
+ debug("Transport rate limit. Will not send item.", span);
55
+ return;
56
+ }
57
+
58
+ spanBatcher.send(span);
59
+ }
60
+
61
+ function sendSpans(spans: Span[]): void {
62
+ send("/v1/traces", {
63
+ resourceSpans: [
64
+ {
65
+ resource: vars.resource,
66
+ scopeSpans: [
67
+ {
68
+ scope: vars.scope,
69
+ spans: spans,
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ } satisfies ExportTraceServiceRequest).catch((err) => {
75
+ error("Failed to transmit spans", err);
76
+ });
77
+ }
@@ -0,0 +1,3 @@
1
+ export const NO_VALUE_FALLBACK = "undefined";
2
+
3
+ export const INIT_MESSAGE = "Initializing Dash0 Web SDK";
@@ -0,0 +1,25 @@
1
+ import { noop } from "./fn";
2
+
3
+ type Logger = (...args: any[]) => void;
4
+
5
+ export const log: Logger = createLogger("log");
6
+ export const info: Logger = createLogger("info");
7
+ export const warn: Logger = createLogger("warn");
8
+ export const error: Logger = createLogger("error");
9
+ export const debug: Logger = createLogger("debug");
10
+
11
+ function createLogger(method: Extract<keyof Console, "log" | "info" | "warn" | "error" | "debug">): Logger {
12
+ if (typeof console === "undefined" || typeof console.log !== "function" || typeof console.log.apply !== "function") {
13
+ return noop;
14
+ }
15
+
16
+ if (console[method] && typeof console[method].apply === "function") {
17
+ return function () {
18
+ console[method].apply(console, arguments as any);
19
+ };
20
+ }
21
+
22
+ return function () {
23
+ console.log.apply(console, arguments as any);
24
+ };
25
+ }
@@ -0,0 +1,3 @@
1
+ export function noop() {
2
+ // This function is intentionally empty.
3
+ }
@@ -0,0 +1,42 @@
1
+ // aliasing globals for improved minification
2
+
3
+ /* eslint-disable no-restricted-globals */
4
+
5
+ export type WindowType = typeof window;
6
+
7
+ // Avoid blowing up in an ssr context. It is important to check via typeof here because window might not even be declared when imported in ssr.
8
+ export const win: typeof window | undefined = typeof window !== "undefined" ? window : undefined;
9
+ export const doc: typeof window.document | undefined = win?.document;
10
+ export const nav: typeof navigator | undefined = win?.navigator;
11
+ export const loc: typeof location | undefined = typeof location !== "undefined" ? location : undefined;
12
+ export const perf =
13
+ win?.performance || (win as any)?.webkitPerformance || (win as any)?.msPerformance || (win as any)?.mozPerformance;
14
+ export const encodeURIComponent: ((arg: string) => string) | undefined = win?.encodeURIComponent;
15
+ export const fetch = win?.fetch;
16
+ export const localStorage: Storage | null = (function () {
17
+ try {
18
+ return win?.localStorage ?? null;
19
+ } catch {
20
+ // localStorage access is not permitted in certain security modes, e.g.
21
+ // when cookies are completely disabled in web browsers.
22
+ return null;
23
+ }
24
+ })();
25
+ export const sessionStorage: Storage | null = (function () {
26
+ try {
27
+ return win?.sessionStorage ?? null;
28
+ } catch {
29
+ // sessionStorage access is not permitted in certain security modes, e.g.
30
+ // when cookies are completely disabled in web browsers.
31
+ return null;
32
+ }
33
+ })();
34
+
35
+ /**
36
+ * Exposed via this module to enable testing.
37
+ */
38
+ export function sendBeacon(url: string, data: string): boolean {
39
+ return nav?.sendBeacon(url, data) ?? false;
40
+ }
41
+
42
+ /* eslint-enable no-restricted-globals */
@@ -0,0 +1,17 @@
1
+ export const SPAN_ID_BYTES = 8;
2
+ export const TRACE_ID_BYTES = 16;
3
+ export const PAGE_LOAD_ID_BYTES = TRACE_ID_BYTES;
4
+ export const SESSION_ID_BYTES = TRACE_ID_BYTES;
5
+ export const TAB_ID_BYTES = SPAN_ID_BYTES;
6
+
7
+ const SHARED_CHAR_CODES_ARRAY = Array(32);
8
+ export function generateUniqueId(byteCount: number): string {
9
+ for (let i = 0; i < byteCount * 2; i++) {
10
+ SHARED_CHAR_CODES_ARRAY[i] = Math.floor(Math.random() * 16) + 48;
11
+ // valid hex characters in the range 48-57 and 97-102
12
+ if (SHARED_CHAR_CODES_ARRAY[i] >= 58) {
13
+ SHARED_CHAR_CODES_ARRAY[i] += 39;
14
+ }
15
+ }
16
+ return String.fromCharCode.apply(null, SHARED_CHAR_CODES_ARRAY.slice(0, byteCount * 2));
17
+ }
@@ -0,0 +1,41 @@
1
+ import { expect, describe, it } from "vitest";
2
+ import { generateUniqueId } from "./id";
3
+
4
+ // Create a wrapper function for testing
5
+ function generateId(length: number): string {
6
+ if (typeof length !== "number") {
7
+ throw new Error("Length must be a number");
8
+ }
9
+
10
+ if (length < 1) {
11
+ throw new Error("Length must be greater than 0");
12
+ }
13
+
14
+ return generateUniqueId(Math.ceil(length / 2)).substring(0, length);
15
+ }
16
+
17
+ describe("generateId", () => {
18
+ it("returns a string of the expected length", () => {
19
+ const id = generateId(10);
20
+ expect(id).toHaveLength(10);
21
+ });
22
+
23
+ it("returns a unique ID on each call", () => {
24
+ const id1 = generateId(10);
25
+ const id2 = generateId(10);
26
+ expect(id1).not.toBe(id2);
27
+ });
28
+
29
+ it("throws an error if length is less than 1", () => {
30
+ expect(() => generateId(0)).toThrow("Length must be greater than 0");
31
+ });
32
+
33
+ it("throws an error if length is not a number", () => {
34
+ expect(() => generateId("abc" as any)).toThrow("Length must be a number");
35
+ });
36
+
37
+ it("handles large lengths without errors", () => {
38
+ const id = generateId(1000);
39
+ expect(id).toHaveLength(1000);
40
+ });
41
+ });