@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,164 @@
1
+ import { debug, observeResourcePerformance, win } from "../../utils";
2
+ import { isUrlIgnored, matchesAny } from "../../utils/ignore-rules";
3
+ import { addAttribute, setSpanStatus, addTraceContextHttpHeaders, endSpan, errorToSpanStatus, recordException, startSpan, } from "../../utils/otel";
4
+ import { HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, } from "../../semantic-conventions";
5
+ import { isSameOrigin, wrap, parseUrl } from "../../utils";
6
+ import { vars } from "../../vars";
7
+ import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
8
+ import { sendSpan } from "../../transport";
9
+ import { addResourceNetworkEvents, addResourceSize, HTTP_METHOD_OTHER, isWellKnownHttpMethod } from "./utils";
10
+ import { addCommonAttributes } from "../../attributes";
11
+ export function instrumentFetch() {
12
+ if (!win || !win.fetch || !win.Request) {
13
+ debug("Browser does not support the Fetch API, skipping instrumentation");
14
+ return;
15
+ }
16
+ wrap(win, "fetch", wrapFetch);
17
+ }
18
+ // eslint-disable-next-line no-restricted-globals -- only used as type here
19
+ function wrapFetch(original) {
20
+ return async function fetchWithInstrumentation(input, init) {
21
+ let copyOfInit = init ? Object.assign({}, init) : init;
22
+ let body = null;
23
+ if (copyOfInit?.body) {
24
+ body = copyOfInit.body;
25
+ copyOfInit.body = undefined;
26
+ }
27
+ const request = new Request(input, copyOfInit);
28
+ if (body && copyOfInit) {
29
+ copyOfInit.body = body;
30
+ }
31
+ const url = request.url;
32
+ if (isUrlIgnored(url)) {
33
+ debug(`Not creating span for fetch call because the url is ignored, URL: ${url}`);
34
+ return original(input instanceof Request ? request : input, init);
35
+ }
36
+ // https://fetch.spec.whatwg.org/#concept-request-method
37
+ // We'll match methods case insensitive here to make the user experience a bit less painful
38
+ const originalMethod = request.method ?? "GET";
39
+ const isWellKnownMethod = isWellKnownHttpMethod(originalMethod);
40
+ const isWellKnownMethodMatchingLeniently = isWellKnownHttpMethod(originalMethod.toUpperCase());
41
+ const method = isWellKnownMethodMatchingLeniently ? originalMethod.toUpperCase() : HTTP_METHOD_OTHER;
42
+ const span = startSpan(`HTTP ${method}`);
43
+ // The url namespace of a http span has a different meaning than for common rum signals.
44
+ addCommonAttributes(span.attributes, { url });
45
+ addGraphQlProperties(input, init, span);
46
+ addAttribute(span.attributes, HTTP_REQUEST_METHOD, method);
47
+ if (!isWellKnownMethod) {
48
+ addAttribute(span.attributes, HTTP_REQUEST_METHOD_ORIGINAL, originalMethod);
49
+ }
50
+ const shouldSetCorrelationHeaders = isSameOrigin(url) || matchesAny(vars.propagateTraceHeadersCorsURLs, url);
51
+ if (shouldSetCorrelationHeaders) {
52
+ if (copyOfInit?.headers) {
53
+ // ensure we have a unified container for the headers
54
+ copyOfInit.headers = new Headers(copyOfInit.headers);
55
+ addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
56
+ }
57
+ else if (input instanceof Request) {
58
+ addTraceContextHttpHeaders(request.headers.append, request.headers, span);
59
+ }
60
+ else {
61
+ if (!copyOfInit) {
62
+ copyOfInit = {};
63
+ }
64
+ copyOfInit.headers = new Headers();
65
+ addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
66
+ }
67
+ }
68
+ tryCaptureHttpHeaders(request.headers, span, (k) => httpRequestHeaderKey(k));
69
+ const performanceObserver = observeResourcePerformance({
70
+ // We match on both fetch and XHR here to support polyfills
71
+ resourceMatcher: ({ initiatorType, name }) => (initiatorType === "fetch" || initiatorType === "xmlhttprequest") && name === parseUrl(url).href,
72
+ maxWaitForResourceMillis: vars.maxWaitForResourceTimingsMillis,
73
+ maxToleranceForResourceTimingsMillis: vars.maxToleranceForResourceTimingsMillis,
74
+ onEnd: ({ duration, resource }) => {
75
+ if (resource) {
76
+ addResourceNetworkEvents(span, resource);
77
+ addResourceSize(span, resource);
78
+ }
79
+ // duration is millis we need to convert to nanos
80
+ sendSpan(endSpan(span, undefined, duration * 1000000));
81
+ },
82
+ });
83
+ performanceObserver.start();
84
+ try {
85
+ const response = await original(input instanceof Request ? request : input, copyOfInit);
86
+ addResponseData(span, response);
87
+ // We use a separate promise here because this needs to happen in parallel to application code consuming the response
88
+ waitForFullResponse(response)
89
+ .then(() => performanceObserver.end())
90
+ .catch((e) => {
91
+ performanceObserver.cancel();
92
+ endSpanOnError(span, e);
93
+ });
94
+ return response;
95
+ }
96
+ catch (e) {
97
+ performanceObserver.cancel();
98
+ endSpanOnError(span, e);
99
+ throw e;
100
+ }
101
+ };
102
+ }
103
+ // @ts-expect-error -- WIP
104
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- WIP
105
+ function addGraphQlProperties(input, init, span) {
106
+ try {
107
+ if (!isGraphQLQuery(input, init))
108
+ return;
109
+ }
110
+ catch (e) {
111
+ debug("failed to analyze request for GraphQL insights", e, input, init);
112
+ }
113
+ }
114
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- WIP
115
+ function isGraphQLQuery(input, init) {
116
+ /**
117
+ * TODO: Add GraphQL support.
118
+ * GraphQL queries are either POST or GET requests.
119
+ * Identified by either setting Accept=application/graphql-response+json or the url matching a config field for graphql urls.
120
+ * See: https://graphql.github.io/graphql-over-http/draft/
121
+ * GET requests are queries carrying a query search param
122
+ * POST requests are either queries or mutations, See: https://graphql.org/learn/serving-over-http/#post-request-and-body
123
+ * and https://github.com/instana/weasel/blob/main/lib/hooks/Fetch.ts#L201
124
+ */
125
+ return false;
126
+ }
127
+ function tryCaptureHttpHeaders(headers, span, getAttributeKey) {
128
+ try {
129
+ headers.forEach((value, key) => {
130
+ if (vars.headersToCapture.some((rxp) => rxp.test(key))) {
131
+ addAttribute(span.attributes, getAttributeKey(key), value);
132
+ }
133
+ });
134
+ }
135
+ catch (_e) {
136
+ debug("unable to capture http headers due to CORS policy");
137
+ }
138
+ }
139
+ function addResponseData(span, response) {
140
+ const status = response.status;
141
+ setSpanStatus(span, status >= 200 && status < 400 ? SPAN_STATUS_UNSET : SPAN_STATUS_ERROR, response.statusText);
142
+ addAttribute(span.attributes, HTTP_RESPONSE_STATUS_CODE, String(status));
143
+ tryCaptureHttpHeaders(response.headers, span, (k) => httpResponseHeaderKey(k));
144
+ }
145
+ function waitForFullResponse(response) {
146
+ return new Promise((resolve) => {
147
+ const clonedResponse = response.clone();
148
+ const body = clonedResponse.body;
149
+ if (!body)
150
+ return resolve();
151
+ const reader = body.getReader();
152
+ const read = async () => {
153
+ const { done } = await reader.read();
154
+ if (done)
155
+ return resolve();
156
+ return read();
157
+ };
158
+ return read();
159
+ });
160
+ }
161
+ function endSpanOnError(span, error) {
162
+ recordException(span, error);
163
+ sendSpan(endSpan(span, errorToSpanStatus(error), undefined));
164
+ }
@@ -0,0 +1,35 @@
1
+ import { addAttribute, addSpanEvent } from "../../utils/otel";
2
+ import { domHRTimestampToNanos, hasKey, PerformanceTimingNames } from "../../utils";
3
+ import { HTTP_RESPONSE_BODY_SIZE } from "../../semantic-conventions";
4
+ // SEE: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/http.md?plain=1#L67
5
+ const KNOWN_HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
6
+ export const HTTP_METHOD_OTHER = "_OTHER";
7
+ export function isWellKnownHttpMethod(method) {
8
+ return KNOWN_HTTP_METHODS.includes(method);
9
+ }
10
+ export function addResourceNetworkEvents(span, resource) {
11
+ const ignoreZeros = resource.startTime !== 0;
12
+ addSpanNetworkEvent(span, PerformanceTimingNames.FETCH_START, resource, ignoreZeros);
13
+ addSpanNetworkEvent(span, PerformanceTimingNames.DOMAIN_LOOKUP_START, resource, ignoreZeros);
14
+ addSpanNetworkEvent(span, PerformanceTimingNames.DOMAIN_LOOKUP_END, resource, ignoreZeros);
15
+ addSpanNetworkEvent(span, PerformanceTimingNames.CONNECT_START, resource, ignoreZeros);
16
+ addSpanNetworkEvent(span, PerformanceTimingNames.SECURE_CONNECTION_START, resource, ignoreZeros);
17
+ addSpanNetworkEvent(span, PerformanceTimingNames.CONNECT_END, resource, ignoreZeros);
18
+ addSpanNetworkEvent(span, PerformanceTimingNames.REQUEST_START, resource, ignoreZeros);
19
+ addSpanNetworkEvent(span, PerformanceTimingNames.RESPONSE_START, resource, ignoreZeros);
20
+ addSpanNetworkEvent(span, PerformanceTimingNames.RESPONSE_END, resource, ignoreZeros);
21
+ }
22
+ function addSpanNetworkEvent(span, propertyName, resource, ignoreZeros = true) {
23
+ if (!hasKey(resource, propertyName) ||
24
+ typeof resource[propertyName] !== "number" ||
25
+ (ignoreZeros && resource[propertyName] === 0)) {
26
+ return;
27
+ }
28
+ addSpanEvent(span, propertyName, domHRTimestampToNanos(resource[propertyName]));
29
+ }
30
+ export function addResourceSize(span, resource) {
31
+ const encodedLength = resource.encodedBodySize;
32
+ if (encodedLength != undefined) {
33
+ addAttribute(span.attributes, HTTP_RESPONSE_BODY_SIZE, encodedLength);
34
+ }
35
+ }
@@ -0,0 +1,44 @@
1
+ import { addAttribute, getTraceContextForPageLoad } from "../../utils/otel";
2
+ import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES, PAGE_VIEW_CHANGE_STATE, PAGE_VIEW_CHANGE_STATE_VALUES, PAGE_VIEW_TYPE, PAGE_VIEW_TYPE_VALUES, } from "../../semantic-conventions";
3
+ import { doc, NO_VALUE_FALLBACK } from "../../utils";
4
+ import { addCommonAttributes } from "../../attributes";
5
+ import { sendLog } from "../../transport";
6
+ import { vars } from "../../vars";
7
+ function getPageViewMeta(url) {
8
+ if (!url)
9
+ return {};
10
+ return vars.pageViewInstrumentation.generateMetadata?.(url) ?? {};
11
+ }
12
+ export function transmitPageViewEvent(timeUnixNano, url, virtual, replaced) {
13
+ const meta = getPageViewMeta(url);
14
+ const attributes = [];
15
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.PAGE_VIEW);
16
+ if (meta.attributes) {
17
+ Object.entries(meta.attributes).forEach(([key, value]) => addAttribute(attributes, key, value));
18
+ }
19
+ addCommonAttributes(attributes, { url });
20
+ const bodyAttributes = [];
21
+ addAttribute(bodyAttributes, "title", meta.title ?? doc?.title ?? NO_VALUE_FALLBACK);
22
+ if (doc?.referrer) {
23
+ addAttribute(bodyAttributes, "referrer", doc.referrer);
24
+ }
25
+ addAttribute(bodyAttributes, PAGE_VIEW_TYPE, virtual ? PAGE_VIEW_TYPE_VALUES.VIRTUAL : PAGE_VIEW_TYPE_VALUES.INITIAL);
26
+ addAttribute(bodyAttributes, PAGE_VIEW_CHANGE_STATE, replaced ? PAGE_VIEW_CHANGE_STATE_VALUES.REPLACE : PAGE_VIEW_CHANGE_STATE_VALUES.PUSH);
27
+ const log = {
28
+ timeUnixNano: timeUnixNano,
29
+ attributes: attributes,
30
+ severityNumber: LOG_SEVERITIES.INFO,
31
+ severityText: "INFO",
32
+ body: {
33
+ kvlistValue: {
34
+ values: bodyAttributes,
35
+ },
36
+ },
37
+ };
38
+ const traceContext = getTraceContextForPageLoad();
39
+ if (traceContext) {
40
+ log.traceId = traceContext.traceId;
41
+ log.spanId = traceContext.spanId;
42
+ }
43
+ sendLog(log);
44
+ }
@@ -0,0 +1,6 @@
1
+ import { startPageLoadInstrumentation } from "./page-load";
2
+ import { startPageTransitionInstrumentation } from "./page-transition";
3
+ export function startNavigationInstrumentation() {
4
+ startPageLoadInstrumentation();
5
+ startPageTransitionInstrumentation();
6
+ }
@@ -0,0 +1,83 @@
1
+ import { addEventListener, debug, doc, win, roundToTwoDecimals, error, toNanosTs, getTimeOrigin } from "../../utils";
2
+ import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../../semantic-conventions";
3
+ import { sendLog } from "../../transport";
4
+ import { getTraceContextForPageLoad, addAttribute } from "../../utils/otel";
5
+ import { addCommonAttributes } from "../../attributes";
6
+ import { transmitPageViewEvent } from "./event";
7
+ /**
8
+ * Tracks page loads as per this OTel spec:
9
+ * https://github.com/open-telemetry/semantic-conventions/pull/1910/files
10
+ *
11
+ * Notable difference: The full URL is transmitted as a signal attribute.
12
+ */
13
+ export function startPageLoadInstrumentation() {
14
+ try {
15
+ transmitPageViewEvent(getStartTimeUnixNanos(), win?.location.href ? new URL(win?.location.href) : undefined);
16
+ }
17
+ catch (e) {
18
+ error("Failed to transmit initial page view event", e);
19
+ }
20
+ if (doc?.readyState === "complete") {
21
+ return onLoaded();
22
+ }
23
+ if (win) {
24
+ addEventListener(win, "load", function () {
25
+ // we want to get timing data for loadEventEnd,
26
+ // so asynchronously process this
27
+ setTimeout(onLoaded, 0);
28
+ });
29
+ }
30
+ }
31
+ /**
32
+ * See https://github.com/open-telemetry/semantic-conventions/pull/1919
33
+ */
34
+ function onLoaded() {
35
+ const nt = win?.performance.getEntriesByType("navigation")[0];
36
+ if (!nt) {
37
+ debug("Navigation timings not available. Cannot emit navigation timing log");
38
+ return;
39
+ }
40
+ const attributes = [];
41
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.NAVIGATION_TIMING);
42
+ const bodyAttributes = [];
43
+ addAttribute(bodyAttributes, "name", nt.name);
44
+ addNavigationTiming(bodyAttributes, nt, "responseStatus");
45
+ addNavigationTiming(bodyAttributes, nt, "fetchStart");
46
+ addNavigationTiming(bodyAttributes, nt, "requestStart");
47
+ addNavigationTiming(bodyAttributes, nt, "responseStart");
48
+ addNavigationTiming(bodyAttributes, nt, "domInteractive");
49
+ addNavigationTiming(bodyAttributes, nt, "domContentLoadedEventEnd");
50
+ addNavigationTiming(bodyAttributes, nt, "domComplete");
51
+ addNavigationTiming(bodyAttributes, nt, "loadEventEnd");
52
+ addNavigationTiming(bodyAttributes, nt, "transferSize");
53
+ addNavigationTiming(bodyAttributes, nt, "encodedBodySize");
54
+ addNavigationTiming(bodyAttributes, nt, "decodedBodySize");
55
+ const log = {
56
+ timeUnixNano: getStartTimeUnixNanos(),
57
+ attributes: attributes,
58
+ severityNumber: LOG_SEVERITIES.INFO,
59
+ severityText: "INFO",
60
+ body: {
61
+ kvlistValue: {
62
+ values: bodyAttributes,
63
+ },
64
+ },
65
+ };
66
+ addCommonAttributes(log.attributes);
67
+ const traceContext = getTraceContextForPageLoad();
68
+ if (traceContext) {
69
+ log.traceId = traceContext.traceId;
70
+ log.spanId = traceContext.spanId;
71
+ }
72
+ sendLog(log);
73
+ }
74
+ function addNavigationTiming(attributes, nt, field) {
75
+ // @ts-expect-error index access not recognized by TS, but this makes the code more reusable
76
+ const value = nt[field];
77
+ if (typeof value === "number" && !isNaN(value)) {
78
+ addAttribute(attributes, field, Number.isInteger(value) ? value : roundToTwoDecimals(value));
79
+ }
80
+ }
81
+ function getStartTimeUnixNanos() {
82
+ return toNanosTs(Math.round(getTimeOrigin()));
83
+ }
@@ -0,0 +1,72 @@
1
+ import { debug, nowNanos, win, wrap } from "../../utils";
2
+ import { vars } from "../../vars";
3
+ import { transmitPageViewEvent } from "./event";
4
+ let currentLocation = {};
5
+ let shouldIncludeHashChanges = false;
6
+ let shouldIncludeSearchChanges = false;
7
+ /**
8
+ * Tracks page transitions (virtual page views) as per this OTel spec:
9
+ * https://github.com/open-telemetry/semantic-conventions/pull/1910/files
10
+ *
11
+ * Notable difference: The full URL is transmitted as a signal attribute.
12
+ */
13
+ export function startPageTransitionInstrumentation() {
14
+ if (!win || !win.history) {
15
+ debug("Browser does not support history API, skipping instrumentation");
16
+ return;
17
+ }
18
+ if (!vars.pageViewInstrumentation.trackVirtualPageViews) {
19
+ return;
20
+ }
21
+ shouldIncludeSearchChanges = vars.pageViewInstrumentation.includeParts?.includes("SEARCH") ?? false;
22
+ shouldIncludeHashChanges = vars.pageViewInstrumentation.includeParts?.includes("HASH") ?? false;
23
+ /**
24
+ * Not wrapping history.go, history.backward and history.forward here, because their call signatures don't receive
25
+ * url information. For these cases we can use the popstate and hashchange events.
26
+ */
27
+ wrap(win.history, "replaceState", (original) => function (state, unused, url) {
28
+ onUrlChange(url ? String(url) : undefined, true);
29
+ return original.apply(this, [state, unused, url]);
30
+ });
31
+ wrap(win.history, "pushState", (original) => function (state, unused, url) {
32
+ onUrlChange(url ? String(url) : undefined);
33
+ return original.apply(this, [state, unused, url]);
34
+ });
35
+ win.addEventListener("hashchange", onHashChange);
36
+ win.addEventListener("popstate", onPopState);
37
+ try {
38
+ updateCurrentLocation(new URL(win.location.href));
39
+ }
40
+ catch { }
41
+ }
42
+ function onPopState() {
43
+ // popState event is fired after the location entry has been updated, so the location should already contain the new url.
44
+ onUrlChange(win?.location.href);
45
+ }
46
+ function onHashChange(event) {
47
+ onUrlChange(event.newURL);
48
+ }
49
+ function onUrlChange(url, replaced) {
50
+ if (!url)
51
+ return;
52
+ try {
53
+ const parsedUrl = new URL(url, win?.location.href);
54
+ if (isLocationChange(parsedUrl)) {
55
+ updateCurrentLocation(parsedUrl);
56
+ transmitPageViewEvent(nowNanos(), parsedUrl, true, Boolean(replaced));
57
+ }
58
+ }
59
+ catch (e) {
60
+ debug("Failed to handle url change", e);
61
+ }
62
+ }
63
+ function updateCurrentLocation(url) {
64
+ currentLocation.path = url.pathname;
65
+ currentLocation.search = url.search;
66
+ currentLocation.hash = url.hash;
67
+ }
68
+ function isLocationChange(newUrl) {
69
+ return (newUrl.pathname !== currentLocation.path ||
70
+ (shouldIncludeSearchChanges && newUrl.search !== currentLocation.search) ||
71
+ (shouldIncludeHashChanges && newUrl.hash !== currentLocation.hash));
72
+ }
@@ -0,0 +1,32 @@
1
+ import { onLCP, onINP, onCLS } from "web-vitals";
2
+ import { EVENT_NAME, EVENT_NAMES, LOG_SEVERITIES } from "../semantic-conventions";
3
+ import { nowNanos, roundToTwoDecimals } from "../utils";
4
+ import { sendLog } from "../transport";
5
+ import { addAttribute } from "../utils/otel";
6
+ import { addCommonAttributes } from "../attributes";
7
+ export function startWebVitalsInstrumentation() {
8
+ onLCP(onWebVital);
9
+ onINP(onWebVital);
10
+ onCLS(onWebVital);
11
+ }
12
+ function onWebVital(metric) {
13
+ const attributes = [];
14
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.WEB_VITAL);
15
+ const bodyAttributes = [];
16
+ addAttribute(bodyAttributes, "name", metric.name);
17
+ addAttribute(bodyAttributes, "value", roundToTwoDecimals(metric.value));
18
+ addAttribute(bodyAttributes, "delta", roundToTwoDecimals(metric.delta));
19
+ const log = {
20
+ timeUnixNano: nowNanos(),
21
+ attributes: attributes,
22
+ severityNumber: LOG_SEVERITIES.INFO,
23
+ severityText: "INFO",
24
+ body: {
25
+ kvlistValue: {
26
+ values: bodyAttributes,
27
+ },
28
+ },
29
+ };
30
+ addCommonAttributes(log.attributes);
31
+ sendLog(log);
32
+ }
@@ -0,0 +1,79 @@
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
+ // Misc Signal Attribute Keys
8
+ export const EVENT_NAME = "event.name";
9
+ export const EVENT_TITLE = "dash0.event.title";
10
+ export const PAGE_LOAD_ID = "page.load.id";
11
+ export const SESSION_ID = "session.id";
12
+ export const USER_AGENT = "user_agent.original";
13
+ export const BROWSER_TAB_ID = "browser.tab.id";
14
+ export const WINDOW_WIDTH = "browser.window.width";
15
+ export const WINDOW_HEIGHT = "browser.window.height";
16
+ export const NETWORK_CONNECTION_TYPE = "network.connection.subtype";
17
+ export const EXCEPTION_COMPONENT_STACK = "exception.component_stack";
18
+ // User Attribute Keys
19
+ export const USER_ID = "user.id";
20
+ export const USER_NAME = "user.name";
21
+ export const USER_FULL_NAME = "user.full_name";
22
+ export const USER_EMAIL = "user.email";
23
+ export const USER_HASH = "user.hash";
24
+ export const USER_ROLES = "user.roles";
25
+ // Exception Attribute Keys
26
+ export const EXCEPTION_MESSAGE = "exception.message";
27
+ export const EXCEPTION_TYPE = "exception.type";
28
+ export const EXCEPTION_STACKTRACE = "exception.stacktrace";
29
+ // URL Attribute Keys
30
+ export const URL_DOMAIN = "url.domain";
31
+ export const URL_FRAGMENT = "url.fragment";
32
+ export const URL_FULL = "url.full";
33
+ export const URL_PATH = "url.path";
34
+ export const URL_QUERY = "url.query";
35
+ export const URL_SCHEME = "url.scheme";
36
+ // Http Attribute Keys
37
+ export const HTTP_REQUEST_METHOD = "http.request.method";
38
+ export const HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original";
39
+ export const HTTP_REQUEST_HEADER = "http.request.header";
40
+ export const HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
41
+ export const HTTP_RESPONSE_HEADER = "http.response.header";
42
+ export const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
43
+ // Event Names
44
+ export const EVENT_NAMES = {
45
+ PAGE_VIEW: "browser.page_view",
46
+ NAVIGATION_TIMING: "browser.navigation_timing",
47
+ WEB_VITAL: "browser.web_vital",
48
+ ERROR: "browser.error",
49
+ };
50
+ export const SPAN_EVENT_NAME_EXCEPTION = "exception";
51
+ // Log Severities
52
+ export const LOG_SEVERITIES = {
53
+ UNSPECIFIED: 0,
54
+ TRACE: 1,
55
+ DEBUG: 5,
56
+ INFO: 9,
57
+ WARN: 13,
58
+ ERROR: 17,
59
+ FATAL: 21,
60
+ };
61
+ // Page View Event Attributes
62
+ // SEE: https://github.com/open-telemetry/semantic-conventions/pull/1910/files
63
+ export const PAGE_VIEW_TYPE = "type";
64
+ export const PAGE_VIEW_TYPE_VALUES = {
65
+ INITIAL: 0,
66
+ VIRTUAL: 1,
67
+ };
68
+ export const PAGE_VIEW_CHANGE_STATE = "change_state";
69
+ export const PAGE_VIEW_CHANGE_STATE_VALUES = {
70
+ PUSH: "pushState",
71
+ REPLACE: "replaceState",
72
+ };
73
+ // Span Status
74
+ export const SPAN_STATUS_UNSET = 0;
75
+ export const SPAN_STATUS_OK = 1; // This is here for completion, status ok is reserved for use by application developers
76
+ export const SPAN_STATUS_ERROR = 2;
77
+ // Span Kind
78
+ // See: https://github.com/open-telemetry/opentelemetry-proto/blob/ac3242b03157295e4ee9e616af53b81517b06559/opentelemetry/proto/trace/v1/trace.proto#L143-L169
79
+ export const SPAN_KIND_CLIENT = 3;
@@ -0,0 +1,48 @@
1
+ import { onLastChance } from "../utils/on-last-chance";
2
+ import { doc } from "../utils";
3
+ import { setTimeout } from "../utils/timers";
4
+ const SCHEDULE_DELAY_MILLIS = 1000;
5
+ const MAX_QUEUE_SIZE = 15;
6
+ export function newBatcher(sendInternal) {
7
+ const queuedItems = [];
8
+ let pendingFlushTimeout;
9
+ // We attempt batching of messages to be more efficient on the client, network and
10
+ // server-side. While the connection is either a persistent HTTP 2 connection or
11
+ // an HTTP 1.1 connection with keep-alive, there is still some overhead involved
12
+ // in having many small messages.
13
+ //
14
+ // For this reason we attempt batching. When batching we must be careful to
15
+ // force a transmission when the document is unloaded.
16
+ onLastChance(flush);
17
+ return {
18
+ send(item) {
19
+ if (isWindowHidden()) {
20
+ // We cannot guarantee that we will ever get time to transmit data in a batched
21
+ // format when the window is hidden, as this might occur while the document is
22
+ // being unloaded. Immediately force a transmission in these cases.
23
+ sendInternal([item]);
24
+ return;
25
+ }
26
+ queuedItems.push(item);
27
+ if (queuedItems.length >= MAX_QUEUE_SIZE) {
28
+ flush();
29
+ }
30
+ else if (pendingFlushTimeout == null) {
31
+ pendingFlushTimeout = setTimeout(flush, SCHEDULE_DELAY_MILLIS);
32
+ }
33
+ },
34
+ };
35
+ function flush() {
36
+ if (pendingFlushTimeout != null) {
37
+ clearTimeout(pendingFlushTimeout);
38
+ pendingFlushTimeout = null;
39
+ }
40
+ if (queuedItems.length > 0) {
41
+ sendInternal(queuedItems.slice());
42
+ queuedItems.length = 0;
43
+ }
44
+ }
45
+ }
46
+ function isWindowHidden() {
47
+ return doc?.visibilityState !== "visible";
48
+ }
@@ -0,0 +1,58 @@
1
+ import { vars } from "../vars";
2
+ import { noop, warn, fetch } from "../utils";
3
+ const BEACON_BODY_SIZE_LIMIT = 60000;
4
+ export async function send(path, body) {
5
+ const jsonString = JSON.stringify(body);
6
+ let requestBody = jsonString;
7
+ let byteLength = jsonString.length;
8
+ let isCompressed = false;
9
+ // Try to compress if supported
10
+ if (typeof CompressionStream !== "undefined") {
11
+ requestBody = await compressWithGzip(jsonString);
12
+ byteLength = requestBody.byteLength;
13
+ isCompressed = true;
14
+ }
15
+ // Send to all endpoints in parallel
16
+ await Promise.all(vars.endpoints.map(async (endpoint) => {
17
+ try {
18
+ const url = new URL(endpoint["url"]);
19
+ url.pathname = url.pathname + (url.pathname.endsWith("/") ? path.substring(1) : path);
20
+ const headers = {
21
+ "Content-Type": "application/json",
22
+ Authorization: `Bearer ${endpoint["authToken"]}`,
23
+ };
24
+ if (endpoint.dataset) {
25
+ headers["Dash0-Dataset"] = endpoint["dataset"];
26
+ }
27
+ // Try to compress if supported
28
+ if (isCompressed) {
29
+ headers["Content-Encoding"] = "gzip";
30
+ }
31
+ if (!fetch) {
32
+ warn("Unable to send telemetry, fetch is not defined");
33
+ return;
34
+ }
35
+ const response = await fetch(url, {
36
+ method: "POST",
37
+ headers,
38
+ body: requestBody,
39
+ // The keepalive flag is related to the window.sendBeacon API. This in turn has size limitations.
40
+ keepalive: byteLength <= BEACON_BODY_SIZE_LIMIT,
41
+ });
42
+ // read the body so the connection can be closed
43
+ response.text().catch(noop);
44
+ if (!response.ok) {
45
+ warn(`Failed to send telemetry to ${url}: ${response.status} ${response.statusText}`);
46
+ }
47
+ }
48
+ catch (error) {
49
+ warn(`Error sending telemetry to ${endpoint.url}${path}:`, error);
50
+ }
51
+ }));
52
+ }
53
+ async function compressWithGzip(data) {
54
+ const blob = new Blob([data]);
55
+ const stream = blob.stream();
56
+ const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
57
+ return new Response(compressedStream).arrayBuffer();
58
+ }