@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,77 @@
1
+ import { vars } from "../../vars";
2
+ import { win, WindowType } from "../../utils";
3
+ import {
4
+ addWrappedDomEventListener,
5
+ EventListenerOptionsOrUseCapture,
6
+ popWrappedDomEventListener,
7
+ } from "./async-function-wrapping";
8
+ import { ignoreNextOnErrorEvent } from "./unhandled-error";
9
+
10
+ export function startEventHandlerInstrumentation() {
11
+ if (vars.wrapEventHandlers) {
12
+ wrapEventTarget(win?.EventTarget);
13
+ }
14
+ }
15
+
16
+ function wrapEventTarget(EventTarget: WindowType["EventTarget"] | undefined) {
17
+ if (
18
+ !EventTarget ||
19
+ typeof EventTarget.prototype.addEventListener !== "function" ||
20
+ typeof EventTarget.prototype.removeEventListener !== "function"
21
+ ) {
22
+ return;
23
+ }
24
+
25
+ const originalAddEventListener = EventTarget.prototype.addEventListener;
26
+ const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
27
+
28
+ EventTarget.prototype.addEventListener = function wrappedAddEventListener(
29
+ eventName: string,
30
+ fn: EventListenerOrEventListenerObject | null,
31
+ optionsOrCapture?: EventListenerOptionsOrUseCapture
32
+ ) {
33
+ if (typeof fn !== "function") {
34
+ return originalAddEventListener.apply(this, arguments as any);
35
+ }
36
+
37
+ // non-deopt arguments copy
38
+ const args = new Array(arguments.length);
39
+ for (let i = 0; i < arguments.length; i++) {
40
+ args[i] = arguments[i];
41
+ }
42
+
43
+ args[1] = function wrappedEventListener() {
44
+ try {
45
+ return fn.apply(this, arguments as any);
46
+ } catch (e) {
47
+ reportError(e as any);
48
+ ignoreNextOnErrorEvent();
49
+ throw e;
50
+ }
51
+ };
52
+
53
+ args[1] = addWrappedDomEventListener(this, args[1], eventName, fn, optionsOrCapture);
54
+
55
+ return originalAddEventListener.apply(this, args as any);
56
+ };
57
+
58
+ EventTarget.prototype.removeEventListener = function wrappedRemoveEventListener(
59
+ eventName: string,
60
+ fn: EventListenerOrEventListenerObject | null,
61
+ optionsOrCapture?: EventListenerOptionsOrUseCapture
62
+ ) {
63
+ if (typeof fn !== "function") {
64
+ return originalRemoveEventListener.apply(this, arguments as any);
65
+ }
66
+
67
+ // non-deopt arguments copy
68
+ const args = new Array(arguments.length);
69
+ for (let i = 0; i < arguments.length; i++) {
70
+ args[i] = arguments[i];
71
+ }
72
+
73
+ args[1] = popWrappedDomEventListener(this, eventName, fn, optionsOrCapture, fn);
74
+
75
+ return originalRemoveEventListener.apply(this, args as any);
76
+ };
77
+ }
@@ -0,0 +1,11 @@
1
+ import { startOnErrorInstrumentation } from "./unhandled-error";
2
+ import { startUnhandledRejectionInstrumentation } from "./unhandled-promise-rejection";
3
+ import { startEventHandlerInstrumentation } from "./event-handlers";
4
+ import { startTimerInstrumentation } from "./timers";
5
+
6
+ export function startErrorInstrumentation() {
7
+ startOnErrorInstrumentation();
8
+ startUnhandledRejectionInstrumentation();
9
+ startEventHandlerInstrumentation();
10
+ startTimerInstrumentation();
11
+ }
@@ -0,0 +1,52 @@
1
+ import { vars } from "../../vars";
2
+ import { isRunningZoneJs } from "../../utils/timers";
3
+ import { warn, win, WindowType } from "../../utils";
4
+ import { ignoreNextOnErrorEvent } from "./unhandled-error";
5
+
6
+ export function startTimerInstrumentation() {
7
+ if (vars.wrapTimers) {
8
+ if (isRunningZoneJs) {
9
+ warn(
10
+ "We discovered a usage of Zone.js. In order to avoid any incompatibility issues timer wrapping is not going to be enabled."
11
+ );
12
+ return;
13
+ }
14
+ wrapTimer("setTimeout");
15
+ wrapTimer("setInterval");
16
+ }
17
+ }
18
+
19
+ function wrapTimer(name: "setTimeout" | "setInterval") {
20
+ const original = win?.[name];
21
+ if (typeof original !== "function") {
22
+ // cannot wrap because fn is not a function – should actually never happen
23
+ return;
24
+ }
25
+
26
+ (win as any)[name] = function wrappedTimerSetter(fn: TimerHandler): ReturnType<WindowType[typeof name]> {
27
+ // non-deopt arguments copy
28
+ const args = new Array(arguments.length);
29
+ for (let i = 0; i < arguments.length; i++) {
30
+ args[i] = arguments[i];
31
+ }
32
+ args[0] = wrap(fn);
33
+ return original.apply(this, args as any);
34
+ };
35
+ }
36
+
37
+ function wrap(fn: TimerHandler) {
38
+ if (typeof fn !== "function") {
39
+ // cannot wrap because fn is not a function
40
+ return fn;
41
+ }
42
+
43
+ return function wrappedTimerHandler(this: any) {
44
+ try {
45
+ return fn.apply(this, arguments);
46
+ } catch (e) {
47
+ reportError(e as any);
48
+ ignoreNextOnErrorEvent();
49
+ throw e;
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,187 @@
1
+ import { KeyValue, LogRecord } from "../../../types/otlp";
2
+ import { hasOwnProperty, nowNanos, win } from "../../utils";
3
+ import { ErrorLike, ReportErrorOpts } from "../../../types/errors";
4
+ import { isErrorMessageIgnored } from "../../utils/ignore-rules";
5
+ import { sendLog } from "../../transport";
6
+ import {
7
+ EVENT_NAME,
8
+ EVENT_NAMES,
9
+ EXCEPTION_COMPONENT_STACK,
10
+ EXCEPTION_MESSAGE,
11
+ EXCEPTION_STACKTRACE,
12
+ EXCEPTION_TYPE,
13
+ LOG_SEVERITIES,
14
+ } from "../../semantic-conventions";
15
+ import { addAttribute } from "../../utils/otel";
16
+ import { addCommonAttributes } from "../../attributes";
17
+
18
+ type TrackedError = {
19
+ seenCount: number;
20
+ transmittedCount: number;
21
+ log: LogRecord;
22
+ };
23
+
24
+ const MAX_ERRORS_TO_REPORT = 100;
25
+ const MAX_STACK_SIZE = 30;
26
+ const MAX_NUMBER_OF_TRACKED_ERRORS = 20;
27
+
28
+ let reportedErrors = 0;
29
+ let numberOfDifferentErrorsSeen = 0;
30
+ let seenErrors: { [key: string]: TrackedError } = {};
31
+ let scheduledTransmissionTimeoutHandle: ReturnType<typeof setTimeout> | null;
32
+
33
+ // We are wrapping global listeners. In these, we are catching and rethrowing errors.
34
+ // In older browsers, rethrowing errors actually manipulates the error objects. As a
35
+ // result, it is not possible to just mark an error as reported. The simplest way to
36
+ // avoid double reporting is to temporarily disable the global onError handler…
37
+ let ignoreNextOnError = false;
38
+
39
+ export function ignoreNextOnErrorEvent() {
40
+ ignoreNextOnError = true;
41
+ }
42
+
43
+ export function startOnErrorInstrumentation() {
44
+ if (!win) return;
45
+
46
+ const globalOnError = win.onerror;
47
+
48
+ win.onerror = function (
49
+ message: string | Event,
50
+ fileName?: string,
51
+ lineNumber?: number,
52
+ columnNumber?: number,
53
+ error?: any
54
+ ) {
55
+ if (ignoreNextOnError as boolean) {
56
+ ignoreNextOnError = false;
57
+ if (typeof globalOnError === "function") {
58
+ return globalOnError.apply(this, arguments as any);
59
+ }
60
+ return;
61
+ }
62
+
63
+ let stack = error && error.stack;
64
+ if (!stack) {
65
+ stack = "at " + fileName + " " + lineNumber;
66
+ if (columnNumber != null) {
67
+ stack += ":" + columnNumber;
68
+ }
69
+ }
70
+ onUnhandledError({ message: String(message), stack });
71
+
72
+ if (typeof globalOnError === "function") {
73
+ return globalOnError.apply(this, arguments as any);
74
+ }
75
+ };
76
+ }
77
+
78
+ export function reportError(error: string | ErrorLike, opts?: ReportErrorOpts) {
79
+ if (!error) {
80
+ return;
81
+ }
82
+
83
+ if (typeof error === "string") {
84
+ onUnhandledError({ message: error, opts });
85
+ } else {
86
+ onUnhandledError({ message: error.message, type: error.name, stack: error.stack, opts });
87
+ }
88
+ }
89
+
90
+ type UnhandledErrorArgs = {
91
+ message: string;
92
+ type?: string;
93
+ stack?: string;
94
+ opts?: ReportErrorOpts;
95
+ };
96
+
97
+ function onUnhandledError({ message, type, stack, opts }: UnhandledErrorArgs) {
98
+ if (!message || reportedErrors > MAX_ERRORS_TO_REPORT) {
99
+ return;
100
+ }
101
+
102
+ if (isErrorMessageIgnored(message)) {
103
+ return;
104
+ }
105
+
106
+ if (numberOfDifferentErrorsSeen >= MAX_NUMBER_OF_TRACKED_ERRORS) {
107
+ seenErrors = {};
108
+ numberOfDifferentErrorsSeen = 0;
109
+ }
110
+
111
+ message = String(message).substring(0, 300);
112
+ stack = shortenStackTrace(stack);
113
+ const location = win?.location.href;
114
+ const key = message + stack + location;
115
+
116
+ let trackedError = seenErrors[key];
117
+ if (trackedError) {
118
+ trackedError.seenCount++;
119
+ } else {
120
+ const attributes: KeyValue[] = [];
121
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.ERROR);
122
+ addAttribute(attributes, EXCEPTION_MESSAGE, message);
123
+ if (type) {
124
+ addAttribute(attributes, EXCEPTION_TYPE, type);
125
+ }
126
+ if (stack) {
127
+ addAttribute(attributes, EXCEPTION_STACKTRACE, stack);
128
+ }
129
+ if (opts?.componentStack) {
130
+ addAttribute(attributes, EXCEPTION_COMPONENT_STACK, opts?.componentStack.substring(0, 2048));
131
+ }
132
+ addCommonAttributes(attributes);
133
+
134
+ trackedError = {
135
+ seenCount: 1,
136
+ transmittedCount: 0,
137
+ log: {
138
+ timeUnixNano: nowNanos(),
139
+ attributes: attributes,
140
+ severityNumber: LOG_SEVERITIES.ERROR,
141
+ severityText: "ERROR",
142
+ body: {
143
+ stringValue: message,
144
+ },
145
+ },
146
+ };
147
+ seenErrors[key as string] = trackedError;
148
+
149
+ numberOfDifferentErrorsSeen++;
150
+ }
151
+
152
+ scheduleTransmission();
153
+ }
154
+
155
+ export function shortenStackTrace(stack?: string): string {
156
+ return String(stack || "")
157
+ .split("\n")
158
+ .slice(0, MAX_STACK_SIZE)
159
+ .join("\n");
160
+ }
161
+
162
+ function scheduleTransmission() {
163
+ if (scheduledTransmissionTimeoutHandle) {
164
+ return;
165
+ }
166
+ scheduledTransmissionTimeoutHandle = setTimeout(send, 1000);
167
+ }
168
+
169
+ function send() {
170
+ if (scheduledTransmissionTimeoutHandle) {
171
+ clearTimeout(scheduledTransmissionTimeoutHandle);
172
+ scheduledTransmissionTimeoutHandle = null;
173
+ }
174
+
175
+ for (const key in seenErrors) {
176
+ if (hasOwnProperty(seenErrors, key)) {
177
+ const seenError = seenErrors[key]!;
178
+ if (seenError.seenCount > seenError.transmittedCount) {
179
+ sendLog(seenError.log);
180
+ reportedErrors++;
181
+ }
182
+ }
183
+ }
184
+
185
+ seenErrors = {};
186
+ numberOfDifferentErrorsSeen = 0;
187
+ }
@@ -0,0 +1,30 @@
1
+ import { win } from "../../utils";
2
+ import { reportError } from "./unhandled-error";
3
+
4
+ const MESSAGE_PREFIX = "Unhandled promise rejection: ";
5
+ const STACK_UNAVAILABLE_MESSAGE = "<unavailable because Promise wasn't rejected with an Error object>";
6
+
7
+ export function startUnhandledRejectionInstrumentation() {
8
+ if (typeof win?.addEventListener === "function") {
9
+ win.addEventListener("unhandledrejection", onUnhandledRejection);
10
+ }
11
+ }
12
+
13
+ export function onUnhandledRejection(event: PromiseRejectionEvent) {
14
+ if (event.reason == null) {
15
+ reportError({
16
+ message: MESSAGE_PREFIX + "<no reason defined>",
17
+ stack: STACK_UNAVAILABLE_MESSAGE,
18
+ });
19
+ } else if (typeof event.reason.message === "string") {
20
+ reportError({
21
+ message: MESSAGE_PREFIX + event.reason.message,
22
+ stack: typeof event.reason.stack === "string" ? event.reason.stack : STACK_UNAVAILABLE_MESSAGE,
23
+ });
24
+ } else if (typeof event.reason !== "object") {
25
+ reportError({
26
+ message: MESSAGE_PREFIX + event.reason,
27
+ stack: STACK_UNAVAILABLE_MESSAGE,
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,194 @@
1
+ import { debug, observeResourcePerformance, win } from "../../utils";
2
+ import { isUrlIgnored, matchesAny } from "../../utils/ignore-rules";
3
+ import {
4
+ addAttribute,
5
+ setSpanStatus,
6
+ addTraceContextHttpHeaders,
7
+ endSpan,
8
+ errorToSpanStatus,
9
+ Exception,
10
+ InProgressSpan,
11
+ recordException,
12
+ startSpan,
13
+ } from "../../utils/otel";
14
+ import {
15
+ HTTP_REQUEST_METHOD,
16
+ HTTP_REQUEST_METHOD_ORIGINAL,
17
+ HTTP_RESPONSE_STATUS_CODE,
18
+ SPAN_STATUS_ERROR,
19
+ SPAN_STATUS_UNSET,
20
+ } from "../../semantic-conventions";
21
+ import { isSameOrigin, wrap, parseUrl } from "../../utils";
22
+ import { vars } from "../../vars";
23
+ import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
24
+ import { sendSpan } from "../../transport";
25
+ import { addResourceNetworkEvents, addResourceSize, HTTP_METHOD_OTHER, isWellKnownHttpMethod } from "./utils";
26
+ import { addCommonAttributes } from "../../attributes";
27
+
28
+ export function instrumentFetch() {
29
+ if (!win || !win.fetch || !win.Request) {
30
+ debug("Browser does not support the Fetch API, skipping instrumentation");
31
+ return;
32
+ }
33
+ wrap(win, "fetch", wrapFetch);
34
+ }
35
+
36
+ // eslint-disable-next-line no-restricted-globals -- only used as type here
37
+ function wrapFetch(original: typeof fetch) {
38
+ return async function fetchWithInstrumentation(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
39
+ let copyOfInit = init ? Object.assign({}, init) : init;
40
+
41
+ let body: BodyInit | null = null;
42
+ if (copyOfInit?.body) {
43
+ body = copyOfInit.body;
44
+ copyOfInit.body = undefined;
45
+ }
46
+
47
+ const request = new Request(input, copyOfInit);
48
+ if (body && copyOfInit) {
49
+ copyOfInit.body = body;
50
+ }
51
+
52
+ const url = request.url;
53
+ if (isUrlIgnored(url)) {
54
+ debug(`Not creating span for fetch call because the url is ignored, URL: ${url}`);
55
+ return original(input instanceof Request ? request : input, init);
56
+ }
57
+
58
+ // https://fetch.spec.whatwg.org/#concept-request-method
59
+ // We'll match methods case insensitive here to make the user experience a bit less painful
60
+ const originalMethod = request.method ?? "GET";
61
+ const isWellKnownMethod = isWellKnownHttpMethod(originalMethod);
62
+ const isWellKnownMethodMatchingLeniently = isWellKnownHttpMethod(originalMethod.toUpperCase());
63
+ const method = isWellKnownMethodMatchingLeniently ? originalMethod.toUpperCase() : HTTP_METHOD_OTHER;
64
+
65
+ const span = startSpan(`HTTP ${method}`);
66
+ // The url namespace of a http span has a different meaning than for common rum signals.
67
+ addCommonAttributes(span.attributes, { url });
68
+ addGraphQlProperties(input, init, span);
69
+ addAttribute(span.attributes, HTTP_REQUEST_METHOD, method);
70
+ if (!isWellKnownMethod) {
71
+ addAttribute(span.attributes, HTTP_REQUEST_METHOD_ORIGINAL, originalMethod);
72
+ }
73
+
74
+ const shouldSetCorrelationHeaders = isSameOrigin(url) || matchesAny(vars.propagateTraceHeadersCorsURLs, url);
75
+ if (shouldSetCorrelationHeaders) {
76
+ if (copyOfInit?.headers) {
77
+ // ensure we have a unified container for the headers
78
+ copyOfInit.headers = new Headers(copyOfInit.headers);
79
+ addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
80
+ } else if (input instanceof Request) {
81
+ addTraceContextHttpHeaders(request.headers.append, request.headers, span);
82
+ } else {
83
+ if (!copyOfInit) {
84
+ copyOfInit = {};
85
+ }
86
+ copyOfInit.headers = new Headers();
87
+ addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
88
+ }
89
+ }
90
+
91
+ tryCaptureHttpHeaders(request.headers, span, (k) => httpRequestHeaderKey(k));
92
+
93
+ const performanceObserver = observeResourcePerformance({
94
+ // We match on both fetch and XHR here to support polyfills
95
+ resourceMatcher: ({ initiatorType, name }) =>
96
+ (initiatorType === "fetch" || initiatorType === "xmlhttprequest") && name === parseUrl(url).href,
97
+ maxWaitForResourceMillis: vars.maxWaitForResourceTimingsMillis,
98
+ maxToleranceForResourceTimingsMillis: vars.maxToleranceForResourceTimingsMillis,
99
+ onEnd: ({ duration, resource }) => {
100
+ if (resource) {
101
+ addResourceNetworkEvents(span, resource);
102
+ addResourceSize(span, resource);
103
+ }
104
+ // duration is millis we need to convert to nanos
105
+ sendSpan(endSpan(span, undefined, duration * 1000000));
106
+ },
107
+ });
108
+
109
+ performanceObserver.start();
110
+ try {
111
+ const response = await original(input instanceof Request ? request : input, copyOfInit);
112
+ addResponseData(span, response);
113
+
114
+ // We use a separate promise here because this needs to happen in parallel to application code consuming the response
115
+ waitForFullResponse(response)
116
+ .then(() => performanceObserver.end())
117
+ .catch((e) => {
118
+ performanceObserver.cancel();
119
+ endSpanOnError(span, e as Exception);
120
+ });
121
+
122
+ return response;
123
+ } catch (e) {
124
+ performanceObserver.cancel();
125
+ endSpanOnError(span, e as Exception);
126
+ throw e;
127
+ }
128
+ };
129
+ }
130
+
131
+ // @ts-expect-error -- WIP
132
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- WIP
133
+ function addGraphQlProperties(input: RequestInfo | URL, init?: RequestInit, span: InProgressSpan) {
134
+ try {
135
+ if (!isGraphQLQuery(input, init)) return;
136
+ } catch (e) {
137
+ debug("failed to analyze request for GraphQL insights", e, input, init);
138
+ }
139
+ }
140
+
141
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- WIP
142
+ function isGraphQLQuery(input: RequestInfo | URL, init?: RequestInit) {
143
+ /**
144
+ * TODO: Add GraphQL support.
145
+ * GraphQL queries are either POST or GET requests.
146
+ * Identified by either setting Accept=application/graphql-response+json or the url matching a config field for graphql urls.
147
+ * See: https://graphql.github.io/graphql-over-http/draft/
148
+ * GET requests are queries carrying a query search param
149
+ * POST requests are either queries or mutations, See: https://graphql.org/learn/serving-over-http/#post-request-and-body
150
+ * and https://github.com/instana/weasel/blob/main/lib/hooks/Fetch.ts#L201
151
+ */
152
+ return false;
153
+ }
154
+
155
+ function tryCaptureHttpHeaders(headers: Headers, span: InProgressSpan, getAttributeKey: (headerKey: string) => string) {
156
+ try {
157
+ headers.forEach((value, key) => {
158
+ if (vars.headersToCapture.some((rxp) => rxp.test(key))) {
159
+ addAttribute(span.attributes, getAttributeKey(key), value);
160
+ }
161
+ });
162
+ } catch (_e) {
163
+ debug("unable to capture http headers due to CORS policy");
164
+ }
165
+ }
166
+
167
+ function addResponseData(span: InProgressSpan, response: Response) {
168
+ const status = response.status;
169
+ setSpanStatus(span, status >= 200 && status < 400 ? SPAN_STATUS_UNSET : SPAN_STATUS_ERROR, response.statusText);
170
+ addAttribute(span.attributes, HTTP_RESPONSE_STATUS_CODE, String(status));
171
+ tryCaptureHttpHeaders(response.headers, span, (k) => httpResponseHeaderKey(k));
172
+ }
173
+
174
+ function waitForFullResponse(response: Response): Promise<void> {
175
+ return new Promise((resolve) => {
176
+ const clonedResponse = response.clone();
177
+ const body = clonedResponse.body;
178
+
179
+ if (!body) return resolve();
180
+
181
+ const reader = body.getReader();
182
+ const read = async () => {
183
+ const { done } = await reader.read();
184
+ if (done) return resolve();
185
+ return read();
186
+ };
187
+ return read();
188
+ });
189
+ }
190
+
191
+ function endSpanOnError(span: InProgressSpan, error: Exception) {
192
+ recordException(span, error);
193
+ sendSpan(endSpan(span, errorToSpanStatus(error), undefined));
194
+ }
@@ -0,0 +1,49 @@
1
+ import { addAttribute, addSpanEvent, InProgressSpan } from "../../utils/otel";
2
+ import { domHRTimestampToNanos, hasKey, PerformanceTimingNames } from "../../utils";
3
+ import { HTTP_RESPONSE_BODY_SIZE } from "../../semantic-conventions";
4
+
5
+ // SEE: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/http.md?plain=1#L67
6
+ const KNOWN_HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
7
+ export const HTTP_METHOD_OTHER = "_OTHER";
8
+
9
+ export function isWellKnownHttpMethod(method: string): boolean {
10
+ return KNOWN_HTTP_METHODS.includes(method);
11
+ }
12
+
13
+ export function addResourceNetworkEvents(span: InProgressSpan, resource: PerformanceResourceTiming) {
14
+ const ignoreZeros = resource.startTime !== 0;
15
+
16
+ addSpanNetworkEvent(span, PerformanceTimingNames.FETCH_START, resource, ignoreZeros);
17
+ addSpanNetworkEvent(span, PerformanceTimingNames.DOMAIN_LOOKUP_START, resource, ignoreZeros);
18
+ addSpanNetworkEvent(span, PerformanceTimingNames.DOMAIN_LOOKUP_END, resource, ignoreZeros);
19
+ addSpanNetworkEvent(span, PerformanceTimingNames.CONNECT_START, resource, ignoreZeros);
20
+ addSpanNetworkEvent(span, PerformanceTimingNames.SECURE_CONNECTION_START, resource, ignoreZeros);
21
+ addSpanNetworkEvent(span, PerformanceTimingNames.CONNECT_END, resource, ignoreZeros);
22
+ addSpanNetworkEvent(span, PerformanceTimingNames.REQUEST_START, resource, ignoreZeros);
23
+ addSpanNetworkEvent(span, PerformanceTimingNames.RESPONSE_START, resource, ignoreZeros);
24
+ addSpanNetworkEvent(span, PerformanceTimingNames.RESPONSE_END, resource, ignoreZeros);
25
+ }
26
+
27
+ function addSpanNetworkEvent(
28
+ span: InProgressSpan,
29
+ propertyName: string,
30
+ resource: PerformanceResourceTiming,
31
+ ignoreZeros: boolean = true
32
+ ) {
33
+ if (
34
+ !hasKey(resource, propertyName) ||
35
+ typeof resource[propertyName] !== "number" ||
36
+ (ignoreZeros && resource[propertyName] === 0)
37
+ ) {
38
+ return;
39
+ }
40
+
41
+ addSpanEvent(span, propertyName, domHRTimestampToNanos(resource[propertyName]));
42
+ }
43
+
44
+ export function addResourceSize(span: InProgressSpan, resource: PerformanceResourceTiming) {
45
+ const encodedLength = resource.encodedBodySize;
46
+ if (encodedLength != undefined) {
47
+ addAttribute(span.attributes, HTTP_RESPONSE_BODY_SIZE, encodedLength);
48
+ }
49
+ }
@@ -0,0 +1,66 @@
1
+ import { KeyValue, LogRecord } from "../../../types/otlp";
2
+ import { addAttribute, getTraceContextForPageLoad } from "../../utils/otel";
3
+ import {
4
+ EVENT_NAME,
5
+ EVENT_NAMES,
6
+ LOG_SEVERITIES,
7
+ PAGE_VIEW_CHANGE_STATE,
8
+ PAGE_VIEW_CHANGE_STATE_VALUES,
9
+ PAGE_VIEW_TYPE,
10
+ PAGE_VIEW_TYPE_VALUES,
11
+ } from "../../semantic-conventions";
12
+ import { doc, NO_VALUE_FALLBACK } from "../../utils";
13
+ import { addCommonAttributes } from "../../attributes";
14
+ import { sendLog } from "../../transport";
15
+ import { PageViewMeta, vars } from "../../vars";
16
+
17
+ function getPageViewMeta(url?: URL): PageViewMeta {
18
+ if (!url) return {};
19
+
20
+ return vars.pageViewInstrumentation.generateMetadata?.(url) ?? {};
21
+ }
22
+
23
+ export function transmitPageViewEvent(timeUnixNano: string, url?: URL, virtual?: boolean, replaced?: boolean) {
24
+ const meta = getPageViewMeta(url);
25
+
26
+ const attributes: KeyValue[] = [];
27
+ addAttribute(attributes, EVENT_NAME, EVENT_NAMES.PAGE_VIEW);
28
+
29
+ if (meta.attributes) {
30
+ Object.entries(meta.attributes).forEach(([key, value]) => addAttribute(attributes, key, value));
31
+ }
32
+ addCommonAttributes(attributes, { url });
33
+
34
+ const bodyAttributes: KeyValue[] = [];
35
+ addAttribute(bodyAttributes, "title", meta.title ?? doc?.title ?? NO_VALUE_FALLBACK);
36
+ if (doc?.referrer) {
37
+ addAttribute(bodyAttributes, "referrer", doc.referrer);
38
+ }
39
+
40
+ addAttribute(bodyAttributes, PAGE_VIEW_TYPE, virtual ? PAGE_VIEW_TYPE_VALUES.VIRTUAL : PAGE_VIEW_TYPE_VALUES.INITIAL);
41
+ addAttribute(
42
+ bodyAttributes,
43
+ PAGE_VIEW_CHANGE_STATE,
44
+ replaced ? PAGE_VIEW_CHANGE_STATE_VALUES.REPLACE : PAGE_VIEW_CHANGE_STATE_VALUES.PUSH
45
+ );
46
+
47
+ const log: LogRecord = {
48
+ timeUnixNano: timeUnixNano,
49
+ attributes: attributes,
50
+ severityNumber: LOG_SEVERITIES.INFO,
51
+ severityText: "INFO",
52
+ body: {
53
+ kvlistValue: {
54
+ values: bodyAttributes,
55
+ },
56
+ },
57
+ };
58
+
59
+ const traceContext = getTraceContextForPageLoad();
60
+ if (traceContext) {
61
+ log.traceId = traceContext.traceId;
62
+ log.spanId = traceContext.spanId;
63
+ }
64
+
65
+ sendLog(log);
66
+ }
@@ -0,0 +1,7 @@
1
+ import { startPageLoadInstrumentation } from "./page-load";
2
+ import { startPageTransitionInstrumentation } from "./page-transition";
3
+
4
+ export function startNavigationInstrumentation() {
5
+ startPageLoadInstrumentation();
6
+ startPageTransitionInstrumentation();
7
+ }