update rum
Build and Publish / Metadata (push) Successful in 4s
Build and Publish / BuildAndPushAmd64 (push) Failing after 1m27s

This commit is contained in:
mats committed 2026-11-04 13:04:34 +01:00
1 parent 7b68f60202
commit 193e6c6e39
15 files changed
+2760 -1080

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
import { build } from 'esbuild';
import { fileURLToPath } from 'node:url';
await build({
absWorkingDir: fileURLToPath(new URL('.', import.meta.url)),
entryPoints: ['src/otel-rum.ts', 'src/otel-todo.ts'],
outdir: '../static',
bundle: true,
format: 'iife',
platform: 'browser',
target: ['es2020'],
minify: true,
sourcemap: 'external',
logLevel: 'info',
});
+512
View File
@@ -0,0 +1,512 @@
{
"name": "go-otel-rum",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "go-otel-rum",
"devDependencies": {
"esbuild": "^0.28.1",
"typescript": "^5.9.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "go-otel-rum",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "npm run typecheck && node build.mjs",
"test": "npm run build && node --test ../scripts/otel-rum.test.mjs"
},
"devDependencies": {
"esbuild": "^0.28.1",
"typescript": "^5.9.3"
}
}
+821
View File
@@ -0,0 +1,821 @@
import type { Attributes, BrowserGlobal, Consent, ExceptionLike, RumConfig, VitalMetric } from './types';
interface Breadcrumb {
t: number;
category: string;
message: string;
d?: number;
tid?: string;
}
interface LogRecord {
timeUnixNano: string;
severityText: string;
severityNumber: number;
body: { stringValue: string };
attributes: { key: string; value: { stringValue: string } }[];
traceId?: string;
}
(function (global: BrowserGlobal) {
'use strict';
var DEFAULT_BASE_URL = 'https://otel.k6n.net';
// Find the loading script so declarative data-* configuration keeps working.
var scriptEl: HTMLScriptElement | null = null;
try {
scriptEl = (typeof document !== 'undefined') ? (document.currentScript as HTMLScriptElement | null || (function() {
var scripts = document.getElementsByTagName('script');
for (var i = scripts.length - 1; i >= 0; i--) {
if (scripts[i].src && scripts[i].src.indexOf('otel-rum') !== -1) return scripts[i];
}
return null;
})()) : null;
} catch (e) {}
var rawFetch = typeof global.fetch === 'function' ? global.fetch : null;
var rawXHR = typeof global.XMLHttpRequest === 'function' ? global.XMLHttpRequest : null;
function randomHex(len: number) {
var chars = '0123456789abcdef';
var out = '';
for (var i = 0; i < len; i++) {
out += chars[(Math.random() * 16) | 0];
}
return out;
}
// W3C Trace Context ids: 16-byte trace id, 8-byte span id, both hex.
function newTraceId() {
return randomHex(32);
}
function newSpanId() {
return randomHex(16);
}
function isSameOrigin(url: string) {
try {
var resolved = new global.URL(url, global.location.href);
return resolved.origin === global.location.origin;
} catch (e) {
return false;
}
}
function toOtlpAttributes(obj: Attributes) {
var out = [];
for (var key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
var value = obj[key];
if (value === undefined || value === null || value === '') continue;
out.push({ key: key, value: { stringValue: String(value) } });
}
return out;
}
function init(config: RumConfig = {}) {
config = config || {};
var scriptServiceName = (scriptEl && (scriptEl.getAttribute('data-service-name') || scriptEl.getAttribute('data-service'))) || '';
var scriptBaseUrl = (scriptEl && scriptEl.getAttribute('data-base-url')) || '';
var scriptEndpoint = (scriptEl && scriptEl.getAttribute('data-endpoint')) || '';
var scriptIngestKey = (scriptEl && scriptEl.getAttribute('data-ingest-key')) || '';
// Collector base URL resolution:
// 1. Explicit config.baseUrl or scriptEl data-base-url
// 2. Production collector: https://otel.k6n.net
// Do not derive this from the page or script origin: the script may be
// bundled or self-hosted while logs still belong at the collector.
var baseUrl = (config.baseUrl || scriptBaseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
var endpoint = config.endpoint || scriptEndpoint;
if (!endpoint) {
endpoint = baseUrl + '/v1/logs';
} else if (!/^https?:\/\//i.test(endpoint) && !endpoint.startsWith('//')) {
endpoint = baseUrl + '/' + endpoint.replace(/^\/+/, '');
}
var vitalsEndpoint = config.vitalsEndpoint || (scriptEl && scriptEl.getAttribute('data-vitals-endpoint')) || baseUrl + '/v1/vitals';
// The web-vitals build lives next to this script; fall back to the collector's /static.
var vitalsLibUrl =
config.vitalsLibUrl ||
(scriptEl && scriptEl.getAttribute('data-vitals-lib')) ||
(scriptEl && scriptEl.src ? scriptEl.src.replace(/[^\/]*$/, '') + 'web-vitals.attribution.iife.js' : baseUrl + '/static/web-vitals.attribution.iife.js');
function isCollectorUrl(u: string | URL) {
if (!u) return false;
var s = String(u);
if (endpoint && s.indexOf(endpoint) === 0) return true;
if (s.indexOf('/v1/logs') !== -1 || s.indexOf('/v1/traces') !== -1 || s.indexOf('/v1/metrics') !== -1 || s.indexOf('/v1/vitals') !== -1) return true;
return false;
}
var serviceName = config.serviceName || scriptServiceName || 'frontend';
var environment = config.environment || (scriptEl && scriptEl.getAttribute('data-environment')) || '';
var release = config.release || (scriptEl && scriptEl.getAttribute('data-release')) || '';
var ingestKey = config.ingestKey || scriptIngestKey || '';
var consent: Consent = (config.consent || (scriptEl && scriptEl.getAttribute('data-consent'))) === 'granted' ? 'granted' : 'denied';
var devRequested = typeof config.devMode === 'boolean' ? config.devMode : !!(scriptEl && scriptEl.getAttribute('data-dev-mode') === 'true');
var devHosts = config.devModeHosts || ((scriptEl && scriptEl.getAttribute('data-dev-mode-hosts')) || '').split(',').map(function (host) { return host.trim(); }).filter(Boolean);
var devMode = false;
try {
devMode = devRequested && devHosts.indexOf(new global.URL(global.location.href).hostname) !== -1;
} catch (e) {}
var minimalDiagnostics = config.minimalDiagnostics;
var consentListeners: (() => void)[] = [];
var sampleRate = typeof config.sampleRate === 'number' ? config.sampleRate : 1;
var maxBreadcrumbs = config.maxBreadcrumbs || 20;
var captureConsoleErrors = config.captureConsoleErrors !== false;
var autoInstrumentFetch = config.autoInstrumentFetch !== false;
var autoInstrumentXhr = config.autoInstrumentXhr !== false;
var trackSpaRoutes = config.trackSpaRoutes !== false;
var reportHttpFailures = config.reportHttpFailures !== false;
var captureWebVitals = config.captureWebVitals !== false;
var captureWebVitalsWithoutConsent = typeof config.captureWebVitalsWithoutConsent === 'boolean' ? config.captureWebVitalsWithoutConsent : !!(scriptEl && scriptEl.getAttribute('data-vitals-without-consent') === 'true');
var vitalsStarted = false;
var enableTodo = typeof config.enableTodo === 'boolean' ? config.enableTodo : !!(scriptEl && scriptEl.getAttribute('data-enable-todo') === 'true');
var excludeTodo = config.excludeTodo === true || (scriptEl && scriptEl.getAttribute('data-exclude-todo') === 'true');
var todoLibUrl =
config.todoLibUrl ||
(scriptEl && scriptEl.getAttribute('data-todo-lib')) ||
(scriptEl && scriptEl.src ? scriptEl.src.replace(/[^\/]*$/, '') + 'otel-todo.js' : baseUrl + '/static/otel-todo.js');
var injectTraceHeaders = config.injectTraceHeaders !== false;
var traceHeaderOrigins = config.traceHeaderOrigins; // optional array; defaults to same-origin only
// Client-side rate limiting: max events per rolling minute.
var maxEventsPerMinute = config.maxEventsPerMinute || 60;
// Batch size / flush interval for queued records.
var batchSize = config.batchSize || 10;
var flushIntervalMs = config.flushIntervalMs || 5000;
var breadcrumbs: Breadcrumb[] = [];
// Best-effort correlation: the most recently generated trace id is
// attached to the next error report, so an error surfacing shortly
// after a failed request can usually be found alongside it. This is
// not a real per-async-context propagation mechanism.
var lastTraceId = '';
var sessionId = hasDetailedDiagnostics() ? randomHex(16) : '';
// --- Client-side queue, batching and rate limiting -----------------
var queue: LogRecord[] = [];
var flushTimer: number | null = null;
var sentTimestamps: number[] = [];
var eventsDropped = 0;
function setConsent(next: Consent) {
var normalized: Consent = next === 'granted' ? 'granted' : 'denied';
var disableDev = normalized === 'denied' && devMode;
if (consent === normalized && !disableDev) return;
if (disableDev) devMode = false;
consent = normalized;
queue = [];
if (flushTimer) global.clearTimeout(flushTimer);
flushTimer = null;
breadcrumbs = [];
contextAttrs = {};
lastTraceId = '';
sessionId = hasDetailedDiagnostics() ? randomHex(16) : '';
consentListeners.forEach(function (reset) { reset(); });
startVitals();
}
function getConsent(): Consent {
return consent;
}
function hasDetailedDiagnostics() {
return consent === 'granted' || devMode;
}
function getPrivacyMode(): 'counts' | 'minimal' | 'consented' | 'development' {
if (devMode) return 'development';
if (consent === 'granted') return 'consented';
return minimalDiagnostics ? 'minimal' : 'counts';
}
function mayCaptureVitals() {
return captureWebVitals && (hasDetailedDiagnostics() || captureWebVitalsWithoutConsent);
}
function startVitals() {
if (vitalsStarted || !mayCaptureVitals()) return;
vitalsStarted = true;
observeVitals();
}
function underRateLimit() {
var now = Date.now();
// Drop timestamps older than the rolling 60s window.
while (sentTimestamps.length && now - sentTimestamps[0] > 60000) {
sentTimestamps.shift();
}
if (sentTimestamps.length >= maxEventsPerMinute) return false;
sentTimestamps.push(now);
return true;
}
function enqueue(record: LogRecord) {
queue.push(record);
if (queue.length >= batchSize) {
flush();
return;
}
if (!flushTimer) {
flushTimer = global.setTimeout(flush, flushIntervalMs);
}
}
function flush() {
if (flushTimer) {
global.clearTimeout(flushTimer);
flushTimer = null;
}
if (!queue.length) return;
var records = queue.splice(0, queue.length);
ship(records);
}
function ship(records: LogRecord[]) {
var payload = {
resourceLogs: [
{
resource: { attributes: toOtlpAttributes({ 'service.name': serviceName }) },
scopeLogs: [{ scope: { name: 'otel-rum' }, logRecords: records }]
}
]
};
var body = JSON.stringify(payload);
var url = endpoint;
if (ingestKey) {
url += (url.indexOf('?') === -1 ? '?' : '&') + 'key=' + encodeURIComponent(ingestKey);
}
// Use uninstrumented rawFetch with keepalive: true and mode: 'cors'
// to avoid intercepting and looping on our own telemetry requests.
var fetchFn = rawFetch || (typeof global.fetch === 'function' ? global.fetch : null);
if (fetchFn) {
var fetchHeaders: Record<string, string> = { 'Content-Type': 'application/json' };
if (ingestKey) {
fetchHeaders['X-Otel-Ingest-Key'] = ingestKey;
}
fetchFn.call(global, url, {
method: 'POST',
mode: 'cors',
credentials: 'omit',
referrerPolicy: 'no-referrer',
headers: fetchHeaders,
body: body,
keepalive: true
}).catch(function () {});
return;
}
}
var severityNumbers: Record<string, number> = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17, FATAL: 21 };
function buildRecord(severity: string, message: string, attrs?: Attributes, traceId?: string) {
if (!hasDetailedDiagnostics()) {
var anonymousAttrs: Attributes = {
source: 'frontend',
'service.release': release,
'service.environment': environment,
'privacy.mode': 'counts'
};
var errorType = attrs && attrs['exception.type'];
if (typeof errorType === 'string' && ['Error', 'TypeError', 'ReferenceError', 'SyntaxError', 'RangeError', 'URIError', 'EvalError', 'UnhandledRejection', 'HttpFailure', 'ResourceLoadError'].indexOf(errorType) !== -1) {
anonymousAttrs['exception.type'] = errorType;
}
var status = attrs && String(attrs['http.response.status_code'] || '');
if (status && /^[1-5][0-9]{2}$/.test(status)) anonymousAttrs['http.response.status_code'] = status;
var diagnosticMessage = 'Frontend event';
if (minimalDiagnostics) {
try {
var diagnostic = minimalDiagnostics.sanitize({
message: String(message).slice(0, 2000),
type: typeof errorType === 'string' ? errorType.slice(0, 100) : undefined,
stacktrace: attrs && typeof attrs['exception.stacktrace'] === 'string' ? attrs['exception.stacktrace'].slice(0, 10000) : undefined
});
if (diagnostic && typeof diagnostic.message === 'string' && diagnostic.message.trim()) {
diagnosticMessage = diagnostic.message.slice(0, 2000);
anonymousAttrs['privacy.mode'] = 'minimal';
anonymousAttrs['exception.message'] = diagnosticMessage;
if (typeof diagnostic.code === 'string') anonymousAttrs['error.code'] = diagnostic.code.slice(0, 100);
if (typeof diagnostic.stacktrace === 'string') anonymousAttrs['exception.stacktrace'] = diagnostic.stacktrace.slice(0, 10000);
if (minimalDiagnostics.route) anonymousAttrs['http.route'] = minimalDiagnostics.route.slice(0, 300);
}
} catch (e) {}
}
return {
timeUnixNano: String(Date.now() * 1e6),
severityText: Object.prototype.hasOwnProperty.call(severityNumbers, severity) ? severity : 'ERROR',
severityNumber: severityNumbers[severity] || 17,
body: { stringValue: diagnosticMessage },
attributes: toOtlpAttributes(anonymousAttrs)
};
}
var record: LogRecord = {
timeUnixNano: String(Date.now() * 1e6),
severityText: severity,
severityNumber: severityNumbers[severity] || 17,
body: { stringValue: String(message).slice(0, 2000) },
attributes: toOtlpAttributes(
Object.assign(
{},
contextAttrs,
{
source: 'frontend',
'privacy.mode': getPrivacyMode(),
'browser.url': global.location ? global.location.href : '',
'browser.userAgent': global.navigator ? global.navigator.userAgent : '',
'browser.viewport': global.innerWidth ? global.innerWidth + 'x' + global.innerHeight : '',
'session.id': sessionId,
'service.release': release,
'service.environment': environment,
breadcrumbs: breadcrumbs.length ? JSON.stringify(breadcrumbs) : undefined
},
attrs || {}
)
)
};
if (traceId) {
record.traceId = traceId;
}
return record;
}
function send(severity: string, message: string, attrs?: Attributes, traceId?: string) {
if (sampleRate < 1 && Math.random() > sampleRate) return;
if (!underRateLimit()) {
eventsDropped++;
return;
}
enqueue(buildRecord(severity, message, attrs, traceId));
}
// Web Vitals: measured by the vendored `web-vitals` library (attribution
// build, loaded lazily so it never blocks the page) and reported as ONE
// beacon per page view to /v1/vitals when the page is hidden. The
// beacon carries every metric with its attribution (LCP element, INP
// target/breakdown, CLS shift source...). If the page is hidden again
// later (tab switch) and a metric changed - typically INP - a fuller
// snapshot with the same pageView id is sent; the newest one wins.
function observeVitals() {
var pageViewId = randomHex(16);
var vitals: Record<string, { value: unknown; delta: unknown; id: string; attribution: Attributes }> = {};
var navType = '';
var dirty = false;
var sampled = sampleRate >= 1 || Math.random() <= sampleRate;
consentListeners.push(function () {
pageViewId = randomHex(16);
vitals = {};
dirty = false;
});
var picks: Record<string, string[]> = {
LCP: ['target', 'url', 'timeToFirstByte', 'resourceLoadDelay', 'resourceLoadDuration', 'elementRenderDelay'],
INP: ['interactionTarget', 'interactionType', 'inputDelay', 'processingDuration', 'presentationDelay', 'loadState', 'totalScriptDuration', 'totalStyleAndLayoutDuration', 'totalPaintDuration'],
CLS: ['largestShiftTarget', 'largestShiftValue', 'largestShiftTime', 'loadState'],
FCP: ['timeToFirstByte', 'firstByteToFCP', 'loadState'],
TTFB: ['waitingDuration', 'cacheDuration', 'dnsDuration', 'connectionDuration', 'requestDuration']
};
// Values keep 3 decimals: CLS is a tiny fraction and must not be rounded to an integer.
function round(n: unknown) {
return typeof n === 'number' ? Math.round(n * 1000) / 1000 : n;
}
function slim(metric: VitalMetric) {
var out: Attributes = {};
var src = metric.attribution || {};
(picks[metric.name] || []).forEach(function (k) {
if (src[k] !== undefined && src[k] !== null) out[k] = round(src[k]);
});
var script = src.longestScript && src.longestScript.entry;
if (script && script.sourceURL) {
out.longestScriptUrl = script.sourceURL;
if (script.sourceFunctionName) out.longestScriptFunction = script.sourceFunctionName;
}
return out;
}
function onMetric(metric: VitalMetric) {
if (!mayCaptureVitals()) return;
vitals[metric.name] = { value: round(metric.value), delta: round(metric.delta), id: metric.id, attribution: slim(metric) };
navType = metric.navigationType || navType;
dirty = true;
}
function sendVitals() {
if (!mayCaptureVitals() || !dirty || !sampled) return;
dirty = false;
var granted = hasDetailedDiagnostics();
var anonymousMetrics: Record<string, { value: unknown; delta: unknown; attribution: Attributes }> = {};
Object.keys(vitals).forEach(function (name) {
var attribution: Attributes = {};
Object.keys(vitals[name].attribution).forEach(function (key) {
var value = vitals[name].attribution[key];
if (typeof value === 'number' && Number.isFinite(value)) attribution[key] = value;
});
anonymousMetrics[name] = { value: vitals[name].value, delta: vitals[name].delta, attribution: attribution };
});
var body = JSON.stringify({
service: serviceName,
environment: environment,
release: release,
session: sessionId,
pageView: pageViewId,
url: granted && global.location ? global.location.href : '',
ua: granted && global.navigator ? global.navigator.userAgent : '',
viewport: granted && global.innerWidth ? global.innerWidth + 'x' + global.innerHeight : '',
nav: navType,
conn: granted && (global.navigator && global.navigator.connection && global.navigator.connection.effectiveType) || '',
metrics: granted ? vitals : anonymousMetrics
});
var url = vitalsEndpoint;
if (ingestKey) url += (url.indexOf('?') === -1 ? '?' : '&') + 'key=' + encodeURIComponent(ingestKey);
if (rawFetch) {
rawFetch.call(global, url, { method: 'POST', mode: 'cors', credentials: 'omit', referrerPolicy: 'no-referrer', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, body: body, keepalive: true }).catch(function () {});
}
}
var script = global.document.createElement('script');
script.async = true;
script.referrerPolicy = 'no-referrer';
script.src = vitalsLibUrl;
script.onload = function () {
if (!mayCaptureVitals()) {
vitalsStarted = false;
return;
}
var wv = global.webVitals;
if (!wv) return;
// INP durationThreshold stays at the library default; one listener per metric.
[wv.onLCP, wv.onINP, wv.onCLS, wv.onFCP, wv.onTTFB].forEach(function (observe) {
if (typeof observe === 'function') observe(onMetric);
});
// Registered after the library's own hidden listeners, so the
// final LCP/INP/CLS values are already collected when this runs.
global.addEventListener('visibilitychange', function () {
if (global.document.visibilityState === 'hidden') sendVitals();
});
global.addEventListener('pagehide', sendVitals);
// bfcache restore is a new page view for vitals purposes.
global.addEventListener('pageshow', function (e) {
if (e.persisted) {
pageViewId = randomHex(16);
vitals = {};
}
});
};
global.document.head.appendChild(script);
}
function loadTodoWidget() {
var todo = config.todo || {};
function start() {
if (!global.OtelTodo) return;
global.OtelTodo.init({
getConsent: getConsent,
getSessionId: function () { return sessionId; },
baseUrl: baseUrl,
serviceName: serviceName,
ingestKey: ingestKey,
components: todo.components || ((scriptEl && scriptEl.getAttribute('data-components')) || '').split(','),
shortcut: todo.shortcut || (scriptEl && scriptEl.getAttribute('data-shortcut')) || '',
hideButton: todo.hideButton === true || !!(scriptEl && scriptEl.getAttribute('data-hide-button') === 'true')
});
}
if (global.OtelTodo) return start();
var s = global.document.createElement('script');
s.async = true;
s.referrerPolicy = 'no-referrer';
s.src = todoLibUrl;
s.onload = start;
global.document.head.appendChild(s);
}
if (enableTodo && !excludeTodo && global.document) {
function scheduleTodoWidget() {
if (typeof global.requestIdleCallback === 'function') {
global.requestIdleCallback(loadTodoWidget);
} else {
global.setTimeout(loadTodoWidget, 3000);
}
}
if (global.document.readyState === 'complete') scheduleTodoWidget();
else global.addEventListener('load', scheduleTodoWidget, { once: true });
}
// traceHeaders returns a headers object carrying a fresh (or
// continued) W3C traceparent, ready to spread into any HTTP client's
// request headers - fetch, XHR, axios, etc.
function traceHeaders(existingTraceId?: string) {
var traceId = existingTraceId || newTraceId();
var spanId = newSpanId();
lastTraceId = traceId;
return { traceparent: '00-' + traceId + '-' + spanId + '-01' };
}
function captureException(err: unknown, extra?: Attributes) {
var exception = err && typeof err === 'object' ? err as ExceptionLike : undefined;
var message = (exception && exception.message) || String(err);
send(
'ERROR',
message,
Object.assign(
{
'exception.type': (exception && exception.name) || 'Error',
'exception.message': message,
'exception.stacktrace': (exception && exception.stack) || ''
},
extra || {}
),
lastTraceId
);
}
function captureMessage(message: string, severity?: string, extra?: Attributes) {
send(severity || 'ERROR', message, extra, lastTraceId);
}
function setContext(context: Attributes) {
if (!hasDetailedDiagnostics()) return;
// Attach stable context (e.g. { user: 'u-123', plan: 'pro' }) to
// every subsequent event. Values become OTLP attributes prefixed
// with "context.".
contextAttrs = Object.assign({}, contextAttrs, context || {});
flush(); // ensure earlier queued events don't mix contexts ambiguously
}
var contextAttrs: Attributes = {};
function onError(event: Event) {
// Resource load errors (img/script/link) surface as an ErrorEvent-less
// Event targeting the failed element; script errors carry a message.
var target = event.target as HTMLElement | null;
if (target && event.target !== global && target.tagName) {
var tag = String(target.tagName).toLowerCase();
if (tag === 'img' || tag === 'script' || tag === 'link') {
var url = (target as HTMLImageElement).src || (target as HTMLLinkElement).href || '';
captureMessage('Failed to load ' + tag + ': ' + url, 'ERROR', {
'exception.type': 'ResourceLoadError',
'resource.tag': tag,
'resource.url': url
});
}
return;
}
var errorEvent = event as ErrorEvent;
var message = errorEvent.message || 'Script error';
captureException(errorEvent.error || { name: 'Error', message: message }, {
'source.file': errorEvent.filename || '',
'source.line': errorEvent.lineno,
'source.column': errorEvent.colno
});
}
function onUnhandledRejection(event: PromiseRejectionEvent) {
var reason = event.reason;
captureException(reason instanceof Error ? reason : { name: 'UnhandledRejection', message: String(reason) }, {
'exception.type': (reason && reason.name) || 'UnhandledRejection'
});
}
global.addEventListener('error', onError);
// Resource load errors don't bubble - must listen on the capturing phase.
global.addEventListener('error', onError, true);
global.addEventListener('unhandledrejection', onUnhandledRejection);
if (captureConsoleErrors && global.console && typeof global.console.error === 'function') {
var originalConsoleError = global.console.error;
global.console.error = function (...args: unknown[]) {
try {
addBreadcrumb('console.error', args.join(' '));
} catch (e) {
/* ignore */
}
return originalConsoleError.apply(global.console, args);
};
}
function shouldInject(url: string) {
if (!injectTraceHeaders) return false;
if (!traceHeaderOrigins) return isSameOrigin(url);
return traceHeaderOrigins.some(function (o) {
return url.indexOf(o) === 0;
});
}
// httpFailureReport reports a failed HTTP round-trip as an ERROR event,
// so backend 5xx/4xx responses show up in the Issues view even when
// the app code swallows them.
function httpFailureReport(kind: string, method: string, url: string, status?: number, traceId?: string, extra?: Attributes) {
// Issue refresh failures must not create new issues notifications:
// otherwise an expired session can form an API-error feedback loop.
try {
if (new global.URL(url, global.location.href).pathname === '/api/issues') return;
} catch (e) {}
var attrs = Object.assign(
{
'exception.type': 'HttpFailure',
'http.request.method': method,
'url.full': String(url).slice(0, 500),
'http.response.status_code': status === undefined || status === 0 ? '' : String(status)
},
extra || {}
);
captureMessage(
method + ' ' + url + ' failed with ' + (status === undefined || status === 0 ? 'network error' : 'HTTP ' + status),
'ERROR',
attrs
);
}
if (autoInstrumentFetch && typeof global.fetch === 'function') {
var originalFetch = global.fetch;
global.fetch = function (input, requestInit) {
var request = typeof input !== 'string' && 'url' in input ? input : undefined;
var url = typeof input === 'string' ? input : request ? request.url : String(input);
if (isCollectorUrl(url)) {
return originalFetch.call(global, input, requestInit);
}
var method = (requestInit && requestInit.method) || (request && request.method) || 'GET';
var requestTraceId = '';
if (shouldInject(url)) {
requestInit = requestInit || {};
var headers = new global.Headers(requestInit.headers || (request && request.headers) || {});
var th = traceHeaders();
headers.set('traceparent', th.traceparent);
requestTraceId = th.traceparent.slice(3, 35); // trace id portion
requestInit = Object.assign({}, requestInit, { headers: headers });
}
var startedAt = Date.now();
return originalFetch.call(global, input, requestInit).then(
function (res) {
addBreadcrumb('fetch', method + ' ' + url + ' -> ' + res.status, { d: Date.now() - startedAt, tid: requestTraceId });
if (reportHttpFailures && res.status >= 400) {
httpFailureReport('fetch', method, url, res.status, requestTraceId);
}
return res;
},
function (err) {
addBreadcrumb('fetch', method + ' ' + url + ' -> error', { d: Date.now() - startedAt, tid: requestTraceId });
if (reportHttpFailures) {
httpFailureReport('fetch', method, url, 0, requestTraceId);
}
throw err;
}
);
};
}
// XHR instrumentation: trace-header injection + breadcrumbs + failure
// reporting, so legacy code paths (axios older versions, jQuery, etc.)
// get the same visibility as fetch.
if (autoInstrumentXhr && typeof global.XMLHttpRequest === 'function') {
var originalOpen = global.XMLHttpRequest.prototype.open;
var originalSend = global.XMLHttpRequest.prototype.send;
var originalSetRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader;
global.XMLHttpRequest.prototype.open = function (method, url) {
if (isCollectorUrl(url)) {
return originalOpen.apply(this, arguments as unknown as Parameters<XMLHttpRequest['open']>);
}
try {
this.__rum = { method: String(method), url: String(url) };
if (shouldInject(String(url))) {
var th = traceHeaders();
this.__rum.traceId = th.traceparent.slice(3, 35);
originalSetRequestHeader.call(this, 'traceparent', th.traceparent);
}
} catch (e) {
/* ignore */
}
return originalOpen.apply(this, arguments as unknown as Parameters<XMLHttpRequest['open']>);
};
global.XMLHttpRequest.prototype.send = function () {
var xhr = this;
const info = xhr.__rum;
var xhrStartedAt = Date.now();
if (info && info.url) {
var origHandler = xhr.onreadystatechange;
xhr.onreadystatechange = function () {
try {
if (xhr.readyState === 4) {
addBreadcrumb('xhr', info.method + ' ' + info.url + ' -> ' + xhr.status, { d: Date.now() - xhrStartedAt, tid: info.traceId });
if (reportHttpFailures && (xhr.status === 0 || xhr.status >= 400)) {
httpFailureReport('xhr', info.method, info.url, xhr.status, info.traceId);
}
}
} catch (e) {
/* ignore */
}
if (origHandler) return origHandler.apply(this, arguments as unknown as [Event]);
};
}
return originalSend.apply(this, arguments as unknown as Parameters<XMLHttpRequest['send']>);
};
}
// SPA route-change breadcrumbs: history.pushState/replaceState and
// popstate/hashes get recorded so an error report shows which route
// transitions preceded it.
if (trackSpaRoutes && global.history) {
function recordRoute(kind: string) {
addBreadcrumb('navigation', kind + ' ' + (global.location ? global.location.pathname + global.location.search : ''));
}
var originalPush = global.history.pushState;
var originalReplace = global.history.replaceState;
if (typeof originalPush === 'function') {
global.history.pushState = function () {
var result = originalPush.apply(this, arguments as unknown as Parameters<History['pushState']>);
try {
recordRoute('pushState');
} catch (e) {}
return result;
};
}
if (typeof originalReplace === 'function') {
global.history.replaceState = function () {
var result = originalReplace.apply(this, arguments as unknown as Parameters<History['replaceState']>);
try {
recordRoute('replaceState');
} catch (e) {}
return result;
};
}
global.addEventListener('popstate', function () {
try {
recordRoute('popstate');
} catch (e) {}
});
}
// Flush pending events when the page is being hidden or unloaded so
// errors that happen right before navigation are not lost, and emit
// the final CLS value.
global.addEventListener('pagehide', function () {
flush();
});
global.addEventListener('visibilitychange', function () {
if (global.document && global.document.visibilityState === 'hidden') {
flush();
}
});
// First paint: report vitals after observers have a chance to buffer.
startVitals();
// extra.d = request duration in ms, extra.tid = trace id we sent on the
// request. t stays the completion time; the request started at t - d.
function addBreadcrumb(category: string, message: string, extra?: { d?: number; tid?: string }) {
if (!hasDetailedDiagnostics()) return;
var crumb: Breadcrumb = { t: Date.now(), category: String(category), message: String(message) };
if (extra && typeof extra.d === 'number') crumb.d = extra.d;
if (extra && extra.tid) crumb.tid = String(extra.tid);
breadcrumbs.push(crumb);
if (breadcrumbs.length > maxBreadcrumbs) {
breadcrumbs.shift();
}
}
var instance = {
setConsent: setConsent,
getConsent: getConsent,
getPrivacyMode: getPrivacyMode,
captureException: captureException,
captureMessage: captureMessage,
addBreadcrumb: addBreadcrumb,
setContext: setContext,
flush: flush,
newTraceId: newTraceId,
newSpanId: newSpanId,
traceHeaders: traceHeaders
};
// Convenience handle for manual/console verification:
// OtelRUM.last.captureMessage('test from console').
global.OtelRUM.last = instance;
return instance;
}
global.OtelRUM = { init: init, newTraceId: newTraceId, newSpanId: newSpanId, last: undefined };
// Auto-initialize if data attributes are declared on the <script> element
try {
if (typeof document !== 'undefined' && scriptEl && (scriptEl.getAttribute('data-auto-init') === 'true' || scriptEl.getAttribute('data-service-name') || scriptEl.getAttribute('data-service'))) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
global.OtelRUM.init();
});
} else {
global.OtelRUM.init();
}
}
} catch (e) {}
})((typeof window !== 'undefined' ? window : globalThis) as BrowserGlobal);
+338
View File
@@ -0,0 +1,338 @@
/**
* otel-todo.js - file todos / issues straight from the page.
*
* Shows a small floating symbol and listens for a keyboard shortcut. Opens a
* native <dialog> in the bottom-right corner with a text box, an optional
* component picker, and an "Reference an element" toggle that lets you click
* any node on the page (with a hover outline) to attach its CSS selector and
* a short label to the note. POSTs the note (with the current path, hash and
* any picked element) to the collector's /v1/todos endpoint.
*
* <script src="https://otel.example.com/static/otel-todo.js"
* data-service-name="checkout-web"
* data-ingest-key="..."
* data-components="Header,Cart,Payment"></script>
*
* Attributes (or OtelTodo.init({...}) keys): data-base-url, data-service-name
* (the project), data-ingest-key, data-components (comma list, also
* discovered from [data-component] elements on the page), data-shortcut
* (default "alt+shift+t"), data-todo-endpoint, data-hide-button="true".
*/
import type { BrowserGlobal, Consent, TodoConfig, TodoInstance } from './types';
(function (global: BrowserGlobal) {
'use strict';
var DEFAULT_BASE_URL = 'https://otel.k6n.net';
var scriptEl = document.currentScript;
function attr(name: string) {
return (scriptEl && scriptEl.getAttribute(name)) || '';
}
function parseShortcut(s: string) {
var parts = String(s).toLowerCase().split('+');
var sc = { key: parts.pop(), alt: false, shift: false, ctrl: false, meta: false };
parts.forEach(function (p) {
if (p === 'alt' || p === 'option') sc.alt = true;
else if (p === 'shift') sc.shift = true;
else if (p === 'ctrl' || p === 'control') sc.ctrl = true;
else if (p === 'meta' || p === 'cmd') sc.meta = true;
});
return sc;
}
function init(config: TodoConfig = {}): TodoInstance {
config = config || {};
if (global.OtelTodo && global.OtelTodo.instance) return global.OtelTodo.instance;
var baseUrl = (config.baseUrl || attr('data-base-url') || DEFAULT_BASE_URL).replace(/\/+$/, '');
var endpoint = config.endpoint || attr('data-todo-endpoint') || baseUrl + '/v1/todos';
var project = config.serviceName || attr('data-service-name') || attr('data-service') || 'frontend';
var ingestKey = config.ingestKey || attr('data-ingest-key');
var shortcut = parseShortcut(config.shortcut || attr('data-shortcut') || 'alt+shift+t');
var hideButton = config.hideButton || attr('data-hide-button') === 'true';
var staticComponents = config.components || attr('data-components').split(',');
staticComponents = staticComponents.map(function (c) { return c.trim(); }).filter(Boolean);
var consent: Consent = (config.consent || attr('data-consent')) === 'granted' ? 'granted' : 'denied';
function getConsent(): Consent {
return config.getConsent ? config.getConsent() : consent;
}
function setConsent(next: Consent) {
consent = next === 'granted' ? 'granted' : 'denied';
}
var css = document.createElement('style');
css.textContent =
'.otel-todo-btn{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:32px;height:32px;border-radius:50%;' +
'border:1px solid #8886;background:#fffd;color:#333;font:16px/1 system-ui,sans-serif;cursor:pointer;opacity:.55;padding:0}' +
'.otel-todo-btn:hover,.otel-todo-btn:focus-visible{opacity:1}' +
'dialog.otel-todo{position:fixed;inset:auto 16px 60px auto;margin:0;width:min(340px,calc(100vw - 32px));padding:12px;' +
'border:1px solid #8886;border-radius:8px;background:Canvas;color:CanvasText;font:13px system-ui,sans-serif}' +
'dialog.otel-todo *{box-sizing:border-box;font:inherit}' +
'dialog.otel-todo textarea,dialog.otel-todo select{display:block;width:100%;margin:6px 0;padding:6px}' +
'dialog.otel-todo textarea{min-height:90px;resize:vertical}' +
'dialog.otel-todo .row{display:flex;gap:8px;justify-content:flex-end;align-items:center}' +
'dialog.otel-todo .meta{opacity:.65;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'dialog.otel-todo .status{margin-right:auto;font-size:12px}' +
'.otel-todo-pick{display:inline-flex;align-items:center;gap:5px;border:1px solid #8886;border-radius:6px;' +
'background:transparent;color:inherit;padding:5px 8px;margin:0 0 6px;cursor:pointer;font-size:12px}' +
'.otel-todo-pick:hover,.otel-todo-pick:focus-visible{background:#8882}' +
'.otel-todo-pick[aria-pressed="true"]{background:#3b82f633;border-color:#3b82f6aa;color:#3b82f6}' +
'.otel-todo-ref{display:none;align-items:center;gap:6px;margin:0 0 6px;padding:5px 7px;border:1px solid #8886;' +
'border-radius:6px;background:#8881;font:11px/1.3 ui-monospace,monospace}' +
'.otel-todo-ref.show{display:flex}' +
'.otel-todo-ref span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.otel-todo-ref button{border:0;background:none;cursor:pointer;color:inherit;opacity:.6;font:12px/1 inherit;padding:0 2px}' +
'.otel-todo-ref button:hover{opacity:1}' +
'.otel-todo-hover-outline{position:fixed;z-index:2147483645;pointer-events:none;border:2px solid #3b82f6;' +
'background:#3b82f626;border-radius:3px;transition:all .05s linear}' +
'.otel-todo-pick-hint{position:fixed;left:50%;top:16px;transform:translateX(-50%);z-index:2147483647;' +
'background:#3b82f6;color:#fff;font:12px/1 system-ui,sans-serif;padding:7px 12px;border-radius:20px;' +
'box-shadow:0 4px 14px #0004}';
document.head.appendChild(css);
var dlg = document.createElement('dialog');
dlg.className = 'otel-todo';
dlg.setAttribute('aria-label', 'Add todo');
dlg.innerHTML =
'<form method="dialog">' +
'<div class="meta"></div>' +
'<button type="button" class="otel-todo-pick" aria-pressed="false">⌖ Reference an element</button>' +
'<div class="otel-todo-ref"><span></span><button type="button" data-clear-ref aria-label="Remove reference">✕</button></div>' +
'<textarea name="text" placeholder="What needs doing?" required maxlength="4000"></textarea>' +
'<select name="component" aria-label="Component (optional)"></select>' +
'<div class="row"><span class="status" role="status"></span>' +
'<button type="button" data-cancel>Cancel</button><button type="submit">Add</button></div>' +
'</form>';
var form = dlg.firstElementChild as HTMLFormElement;
var textEl = form.elements.namedItem('text') as HTMLTextAreaElement;
var selectEl = form.elements.namedItem('component') as HTMLSelectElement;
var statusEl = form.querySelector<HTMLElement>('.status')!;
var metaEl = form.querySelector<HTMLElement>('.meta')!;
var pickBtn = form.querySelector<HTMLButtonElement>('.otel-todo-pick')!;
var refEl = form.querySelector<HTMLElement>('.otel-todo-ref')!;
var refLabelEl = refEl.querySelector('span')!;
document.body.appendChild(dlg);
// --- element picker: click a node on the page to attach its selector ---
var picked: { selector: string; label: string } | null = null;
var hoverBox: HTMLDivElement | null = null;
var picking = false;
var hint: HTMLDivElement | null = null;
function cssPath(el: Element) {
if (!(el instanceof Element)) return '';
var parts = [];
var node: Element | null = el;
while (node && node.nodeType === 1 && parts.length < 6) {
if (node.id) { parts.unshift('#' + CSS.escape(node.id)); break; }
var part = node.tagName.toLowerCase();
var cls = (node.getAttribute('class') || '').trim().split(/\s+/).filter(Boolean).slice(0, 2);
if (cls.length) part += '.' + cls.map(function (c) { return CSS.escape(c); }).join('.');
var parent: HTMLElement | null = node.parentElement;
if (parent) {
var tagName = node.tagName;
var siblings = Array.from(parent.children).filter(function (child) { return child.tagName === tagName; });
if (siblings.length > 1) part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
}
parts.unshift(part);
node = parent;
}
return parts.join(' > ');
}
function describeElement(el: Element) {
var tag = el.tagName.toLowerCase();
var text = (el.getAttribute('aria-label') || el.textContent || '').replace(/\s+/g, ' ').trim();
if (text.length > 60) text = text.slice(0, 60) + '…';
return { selector: cssPath(el), label: text ? tag + ' – "' + text + '"' : tag };
}
function setRef(ref: { selector: string; label: string } | null) {
picked = ref;
if (ref) {
refLabelEl.textContent = ref.label;
refLabelEl.title = ref.selector;
refEl.classList.add('show');
} else {
refEl.classList.remove('show');
}
}
function ensureHoverBox() {
if (!hoverBox) {
hoverBox = document.createElement('div');
hoverBox.className = 'otel-todo-hover-outline';
document.body.appendChild(hoverBox);
}
return hoverBox;
}
function onPickMove(e: MouseEvent) {
var el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || dlg.contains(el)) { ensureHoverBox().style.display = 'none'; return; }
var r = el.getBoundingClientRect();
var box = ensureHoverBox();
box.style.display = 'block';
box.style.left = r.left + 'px';
box.style.top = r.top + 'px';
box.style.width = r.width + 'px';
box.style.height = r.height + 'px';
}
function onPickClick(e: MouseEvent) {
var el = document.elementFromPoint(e.clientX, e.clientY);
if (!el || dlg.contains(el)) return; // let dialog controls work normally
e.preventDefault();
e.stopPropagation();
setRef(describeElement(el));
stopPicking();
}
function onPickKey(e: KeyboardEvent) {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); stopPicking(); }
}
function startPicking() {
if (picking) return;
picking = true;
pickBtn.setAttribute('aria-pressed', 'true');
pickBtn.textContent = '⌖ Click an element… (Esc to cancel)';
hint = document.createElement('div');
hint.className = 'otel-todo-pick-hint';
hint.textContent = 'Click the element you mean — Esc to cancel';
document.body.appendChild(hint);
document.addEventListener('mousemove', onPickMove, true);
document.addEventListener('click', onPickClick, true);
document.addEventListener('keydown', onPickKey, true);
}
function stopPicking() {
if (!picking) return;
picking = false;
pickBtn.setAttribute('aria-pressed', 'false');
pickBtn.textContent = '⌖ Reference an element';
document.removeEventListener('mousemove', onPickMove, true);
document.removeEventListener('click', onPickClick, true);
document.removeEventListener('keydown', onPickKey, true);
if (hoverBox) { hoverBox.style.display = 'none'; }
if (hint) { hint.remove(); hint = null; }
}
pickBtn.addEventListener('click', function () { picking ? stopPicking() : startPicking(); });
refEl.querySelector('[data-clear-ref]')!.addEventListener('click', function () { setRef(null); });
function components() {
var seen: Record<string, boolean> = {};
var list: string[] = [];
staticComponents.concat(Array.from(document.querySelectorAll('[data-component]'), function (el) {
return el.getAttribute('data-component') || '';
})).forEach(function (c) {
if (c && !seen[c]) { seen[c] = true; list.push(c); }
});
return list;
}
function open() {
if (dlg.open) { textEl.focus(); return; }
selectEl.innerHTML = '';
var none = document.createElement('option');
none.value = '';
none.textContent = 'No component';
selectEl.appendChild(none);
var list = components();
list.forEach(function (c) {
var o = document.createElement('option');
o.value = o.textContent = c;
selectEl.appendChild(o);
});
selectEl.hidden = list.length === 0;
metaEl.textContent = project + ' · ' + location.pathname + location.hash;
statusEl.textContent = '';
setRef(null);
// Non-modal so the page stays usable while the note is being written.
dlg.show();
textEl.focus();
}
function close() {
stopPicking();
if (dlg.open) dlg.close();
}
form.querySelector('[data-cancel]')!.addEventListener('click', close);
dlg.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); form.requestSubmit(); }
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var text = textEl.value.trim();
if (!text) return;
var headers: Record<string, string> = { 'Content-Type': 'application/json' };
var url = endpoint;
if (ingestKey) headers['X-Otel-Ingest-Key'] = ingestKey;
var granted = getConsent() === 'granted';
statusEl.textContent = 'Sending…';
global.fetch(url, {
method: 'POST',
mode: 'cors',
credentials: 'omit',
referrerPolicy: 'no-referrer',
headers: headers,
body: JSON.stringify({
project: project,
text: text,
component: selectEl.value,
path: granted ? location.pathname : '',
hash: granted ? location.hash : '',
url: granted ? location.href : '',
ua: granted ? navigator.userAgent : '',
session: granted && config.getSessionId ? config.getSessionId() : '',
elementSelector: picked ? picked.selector : '',
elementLabel: picked ? picked.label : ''
})
}).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
textEl.value = '';
setRef(null);
close();
}).catch(function (err) {
statusEl.textContent = 'Failed: ' + err.message;
});
});
global.addEventListener('keydown', function (e) {
var k = e.key.toLowerCase();
var code = (e.code || '').replace(/^(Key|Digit)/, '').toLowerCase();
if ((k === shortcut.key || code === shortcut.key) && e.altKey === shortcut.alt && e.shiftKey === shortcut.shift &&
e.ctrlKey === shortcut.ctrl && e.metaKey === shortcut.meta) {
e.preventDefault();
dlg.open ? close() : open();
}
});
if (!hideButton) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'otel-todo-btn';
btn.textContent = '✎';
btn.title = 'Add todo (' + (config.shortcut || attr('data-shortcut') || 'alt+shift+t') + ')';
btn.setAttribute('aria-label', 'Add todo');
btn.addEventListener('click', function () { dlg.open ? close() : open(); });
document.body.appendChild(btn);
}
var instance = { open: open, close: close, getConsent: getConsent, setConsent: setConsent };
global.OtelTodo!.instance = instance;
return instance;
}
global.OtelTodo = { init: init, instance: undefined };
if (scriptEl && (attr('data-auto-init') === 'true' || attr('data-service-name') || attr('data-service'))) {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(); });
else init();
}
})(window);
+132
View File
@@ -0,0 +1,132 @@
export type Consent = 'granted' | 'denied';
export interface DiagnosticEvent {
message: string;
type?: string;
stacktrace?: string;
}
export interface MinimalDiagnostic {
message: string;
code?: string;
stacktrace?: string;
}
export interface MinimalDiagnosticsPolicy {
sanitize(event: DiagnosticEvent): MinimalDiagnostic | null;
route?: string;
}
export interface TodoConfig {
consent?: Consent;
getConsent?: () => Consent;
getSessionId?: () => string;
baseUrl?: string;
endpoint?: string;
serviceName?: string;
ingestKey?: string;
components?: string[];
shortcut?: string;
hideButton?: boolean;
}
export interface RumConfig {
consent?: Consent;
minimalDiagnostics?: MinimalDiagnosticsPolicy;
devMode?: boolean;
devModeHosts?: string[];
baseUrl?: string;
endpoint?: string;
vitalsEndpoint?: string;
vitalsLibUrl?: string;
serviceName?: string;
environment?: string;
release?: string;
ingestKey?: string;
sampleRate?: number;
maxBreadcrumbs?: number;
captureConsoleErrors?: boolean;
autoInstrumentFetch?: boolean;
autoInstrumentXhr?: boolean;
trackSpaRoutes?: boolean;
reportHttpFailures?: boolean;
captureWebVitals?: boolean;
captureWebVitalsWithoutConsent?: boolean;
enableTodo?: boolean;
excludeTodo?: boolean;
todoLibUrl?: string;
todo?: TodoConfig;
injectTraceHeaders?: boolean;
traceHeaderOrigins?: string[];
maxEventsPerMinute?: number;
batchSize?: number;
flushIntervalMs?: number;
}
export type Attributes = Record<string, unknown>;
export interface ExceptionLike {
name?: string;
message?: string;
stack?: string;
}
export interface RumInstance {
setConsent(consent: Consent): void;
getConsent(): Consent;
getPrivacyMode(): 'counts' | 'minimal' | 'consented' | 'development';
captureException(error: unknown, extra?: Attributes): void;
captureMessage(message: string, severity?: string, extra?: Attributes): void;
addBreadcrumb(category: string, message: string, extra?: { d?: number; tid?: string }): void;
setContext(context: Attributes): void;
flush(): void;
newTraceId(): string;
newSpanId(): string;
traceHeaders(existingTraceId?: string): { traceparent: string };
}
export interface RumAPI {
init(config?: RumConfig): RumInstance;
newTraceId(): string;
newSpanId(): string;
last?: RumInstance;
}
export interface TodoInstance {
setConsent(consent: Consent): void;
getConsent(): Consent;
open(): void;
close(): void;
}
export interface TodoAPI {
init(config?: TodoConfig): TodoInstance;
instance?: TodoInstance;
}
export interface VitalMetric {
name: string;
value: number;
delta: number;
id: string;
navigationType?: string;
attribution?: Record<string, unknown> & {
longestScript?: { entry?: { sourceURL?: string; sourceFunctionName?: string } };
};
}
declare global {
interface Window {
OtelRUM: RumAPI;
OtelTodo?: TodoAPI;
webVitals?: Partial<Record<'onLCP' | 'onINP' | 'onCLS' | 'onFCP' | 'onTTFB', (callback: (metric: VitalMetric) => void) => void>>;
}
interface Navigator {
connection?: { effectiveType?: string };
}
interface XMLHttpRequest {
__rum?: { method: string; url: string; traceId?: string };
}
}
export type BrowserGlobal = Window & typeof globalThis;
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"strict": true,
"erasableSyntaxOnly": true,
"skipLibCheck": true,
"rootDir": "src",
"outDir": "../static"
},
"include": ["src/**/*.ts"]
}