From c20a0b77f3e69194864ad38831cba217a4cbb08c Mon Sep 17 00:00:00 2001 From: developer Date: Thu, 17 Sep 2026 10:20:42 +0000 Subject: [PATCH] Add frontend error collector, CORS/ingest guards, and static doc routes (Phase 3) - Add static/otel-rum.js: a framework-free browser script that captures uncaught errors, unhandled promise rejections and failed resource loads, translating them into OTLP log records POSTed to the existing /v1/logs endpoint (source: frontend attribute) so they flow through the same history/search/issue pipeline as backend telemetry. - Add correlation-id support: traceHeaders() (manual) and same-origin fetch auto-instrumentation inject a W3C traceparent header, so a frontend error and the backend spans it triggered share a trace id and can be found together via GET /api/traces/{traceId}. - Add CORS handling (withCORS in cmd/server/main.go) on the OTLP HTTP endpoints, required for any browser-origin POST. - Add an optional shared ingest key (OTEL_INGEST_KEY, via '?key=' or X-Otel-Ingest-Key) and a per-IP rate limit (20 rps/burst 50) on OTLP HTTP ingestion, to scope/protect a public endpoint fed by a browser script. Verified end-to-end (CORS preflight, cross-origin POST, ingest key enforcement, rate limiting) against a running server. - Serve static/otel-rum.js and agents_example.md as static HTTP routes so both the script and the agent instructions doc are fetchable directly from the base URL. - Ignore the separately-cloned otel-ui/ frontend repo (has its own .git; not meant to be nested into this repo's history). Co-Authored-By: Claude Sonnet 5 --- .gitignore | 5 +- Dockerfile | 2 + README.md | 48 +++++ agents_example.md | 2 +- cmd/server/main.go | 40 +++- docs/PLAN.md | 26 ++- internal/collector/collector.go | 16 ++ internal/collector/collector_test.go | 59 ++++++ internal/collector/handlers.go | 31 +++ static/otel-rum.js | 277 +++++++++++++++++++++++++++ 10 files changed, 489 insertions(+), 17 deletions(-) create mode 100644 static/otel-rum.js diff --git a/.gitignore b/.gitignore index 1f639c9..1f6cee1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ ./server .DS_Store otel-agent.log -./semgrep-results.json \ No newline at end of file +./semgrep-results.json +# Separate frontend repo, checked out locally for the Dockerfile's UI build +# stage; not nested into this repo's history. +/otel-ui/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7e5d6d0..055ae8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,8 @@ WORKDIR /home/appuser/ COPY --from=go-builder /go-otel . COPY --from=go-builder /app/otel-ui/dist ./dist +COPY static/ ./static +COPY agents_example.md . # Ensure the appuser owns the files RUN chown -R appuser:appgroup /home/appuser/ diff --git a/README.md b/README.md index 5a70d56..c6ba935 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,14 @@ Errors seen in traces (spans with an error status) and logs (severity - `GET /` - Serves the HTML visualization interface +### Static Routes + +- `GET /static/otel-rum.js` - The frontend error collector script (see + below). +- `GET /agents_example.md` - Serves this repo's agent instructions doc + directly, so an AI agent can fetch it from the base URL before making any + API calls. + ## Usage ### Sending Telemetry Data @@ -156,8 +164,48 @@ curl http://localhost:8080/api/history/traces This returns JSON arrays of historical entries with timestamps. +### Frontend Error Collector (`static/otel-rum.js`) + +A framework-free browser script that captures uncaught errors, unhandled +promise rejections and failed resource loads (img/script/link), and ships +them as OTLP log records to the existing `/v1/logs` endpoint - so they flow +through the same history/search/issue pipeline as backend telemetry, tagged +`source: frontend`. + +```html + + +``` + +`fetch` is auto-instrumented by default (same-origin requests only) to +inject `traceparent` and record request/response breadcrumbs; this can be +disabled via `injectTraceHeaders: false` / `autoInstrumentFetch: false`. +Out of scope for v1: source map resolution (stack traces stay minified), +session replay, PII scrubbing - don't route sensitive data through +breadcrumbs or attributes. + ## Configuration +- `OTELLOG_PATH` - path to the persistent telemetry log file (default + `otel-agent.log`). +- `OTEL_INGEST_KEY` - optional shared key required (via `?key=` or the + `X-Otel-Ingest-Key` header) on `/v1/traces`, `/v1/metrics` and `/v1/logs` + over HTTP. Unset by default (no key required). This is meant to + scope/rate-limit a public ingest endpoint fed by a browser script, not to + provide real authentication - pair it with the built-in per-IP rate + limiting on those endpoints if the endpoint is public. + Currently, the history buffer size is hardcoded to 100 entries. This can be made configurable in future versions. ## Development diff --git a/agents_example.md b/agents_example.md index 86b1557..f85c430 100644 --- a/agents_example.md +++ b/agents_example.md @@ -1,6 +1,6 @@ # AI Agent Debugging with OTEL Collector -This document provides instructions for AI agents on how to utilize the OTEL Collector's debugging features to search and fetch telemetry data (traces, logs, and metrics). +This document provides instructions for AI agents on how to utilize the OTEL Collector's debugging features to search and fetch telemetry data (traces, logs, and metrics). It is also served as-is at `{Base URL}/agents_example.md`, so an agent can fetch it directly before making any API calls. ## Base Configuration - **Base URL**: `https://otel.k6n.net` diff --git a/cmd/server/main.go b/cmd/server/main.go index 9eae2c2..52f8c54 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -149,6 +149,23 @@ func (s *logsService) Export(ctx context.Context, req *logsgrpc.ExportLogsServic return &logsgrpc.ExportLogsServiceResponse{}, nil } +// withCORS allows cross-origin ingestion (e.g. from the otel-rum.js browser +// script running on a third-party site) into the OTLP HTTP endpoints. It's +// deliberately permissive on origin, since this is a public ingest endpoint +// scoped by the ingest key/rate limiting instead of same-origin policy. +func withCORS(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Otel-Ingest-Key") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next(w, r) + } +} + func main() { // Configure history size (default 100, can be made configurable) @@ -162,6 +179,7 @@ func main() { wsServer := websocket.New(coll) // Update collector with wsServer coll.SetBroadcaster(wsServer) + coll.SetIngestKey(os.Getenv("OTEL_INGEST_KEY")) // Start gRPC server go func() { @@ -186,17 +204,19 @@ func main() { // OTLP HTTP mux (standard OTel port 4318) otelMux := http.NewServeMux() - otelMux.HandleFunc("/v1/traces", coll.HandleTraces) - otelMux.HandleFunc("/v1/metrics", coll.HandleMetrics) - otelMux.HandleFunc("/v1/logs", coll.HandleLogs) + otelMux.HandleFunc("/v1/traces", withCORS(coll.HandleTraces)) + otelMux.HandleFunc("/v1/metrics", withCORS(coll.HandleMetrics)) + otelMux.HandleFunc("/v1/logs", withCORS(coll.HandleLogs)) otelMux.HandleFunc("/health", coll.HandleHealth) // UI and API mux (port 8080) uiMux := http.NewServeMux() - // Also register OTLP endpoints on 8080 for backwards compatibility - uiMux.HandleFunc("/v1/traces", coll.HandleTraces) - uiMux.HandleFunc("/v1/metrics", coll.HandleMetrics) - uiMux.HandleFunc("/v1/logs", coll.HandleLogs) + // Also register OTLP endpoints on 8080 for backwards compatibility. + // CORS is enabled here since this is the publicly exposed port (via the + // ingress) that a browser script such as static/otel-rum.js posts to. + uiMux.HandleFunc("/v1/traces", withCORS(coll.HandleTraces)) + uiMux.HandleFunc("/v1/metrics", withCORS(coll.HandleMetrics)) + uiMux.HandleFunc("/v1/logs", withCORS(coll.HandleLogs)) uiMux.HandleFunc("/health", coll.HandleHealth) uiMux.HandleFunc("/api/history", coll.HandleHistory) @@ -209,6 +229,12 @@ func main() { uiMux.HandleFunc("/api/issues", coll.HandleIssues) uiMux.HandleFunc("GET /api/issues/{id}", coll.HandleIssueByID) uiMux.HandleFunc("/ws", wsServer.HandleConnections) + uiMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + // Serve the agent instructions doc as a static route so an AI agent can + // fetch it directly from the base URL before making any API calls. + uiMux.HandleFunc("GET /agents_example.md", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "agents_example.md") + }) uiMux.Handle("/", spaHandler(http.Dir("./dist"))) log.Printf("Starting HTTP server on port 8080 (UI/API)") diff --git a/docs/PLAN.md b/docs/PLAN.md index 355236d..e5d4da3 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -34,14 +34,24 @@ Sentry-like frontend error collector. ## Phase 3 — Frontend error collector (Sentry-style) -- [ ] `static/otel-rum.js`: captures `window.onerror`, +- [x] `static/otel-rum.js`: captures uncaught errors, `unhandledrejection`, resource load failures; translates them to OTLP log records client-side and POSTs to the existing `/v1/logs` endpoint (so Phase 2's issue derivation picks them up for free via a - `source: frontend` attribute). -- [ ] CORS support on the OTLP HTTP endpoints (currently absent — required - for any browser-origin POST). -- [ ] Lightweight ingest key for scoping/rate-limiting noisy pages (not - real auth). -- [ ] Explicitly out of scope for v1: source map resolution, session - replay, PII scrubbing. + `source: frontend` attribute). Verified end-to-end against a running + server: an ingested frontend error shows up in `GET /api/issues` and + `GET /api/traces/{traceId}`. +- [x] Correlation-id injection: `traceHeaders()` (manual) and + auto-injection into same-origin `fetch` calls, using a W3C + `traceparent` header so a backend span sharing that trace id and the + frontend error that triggered it show up together via + `/api/traces/{traceId}`. +- [x] CORS support on the OTLP HTTP endpoints (`withCORS` in + `cmd/server/main.go`), verified with a live cross-origin POST. +- [x] Lightweight ingest key (`OTEL_INGEST_KEY`, checked via + `Collector.checkIngest`) for scoping noisy pages, plus a per-IP rate + limit (20 rps/burst 50) on the OTLP HTTP ingestion endpoints so a + runaway error loop can't hammer the collector. Not real auth. +- [x] Explicitly out of scope for v1: source map resolution, session + replay, PII scrubbing (documented in README.md and the script's + header comment). diff --git a/internal/collector/collector.go b/internal/collector/collector.go index 89a0442..af532b6 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -32,6 +32,9 @@ type Collector struct { subMutex sync.Mutex waitLimiter *ipRateLimiter issues *issues.Tracker + + ingestLimiter *ipRateLimiter + ingestKey string } // New creates a new Collector instance. @@ -45,9 +48,22 @@ func New(broadcaster websocket.Broadcaster, historySize int, store storage.Store // Allow bursts of a few immediate waits, then steady-state ~1 req/sec per IP. waitLimiter: newIPRateLimiter(1, 5), issues: issues.NewTracker(), + // Generous enough for normal exporter batching, but enough to stop a + // runaway client (e.g. a frontend error handler that itself throws, + // looping) from hammering the collector. + ingestLimiter: newIPRateLimiter(20, 50), } } +// SetIngestKey configures an optional shared key that HTTP OTLP ingestion +// (traces/metrics/logs) must present, via a '?key=' query parameter or an +// 'X-Otel-Ingest-Key' header. An empty key (the default) disables the check. +// This is meant to scope/rate-limit a public ingest endpoint (e.g. one fed +// by a browser script), not to provide real authentication. +func (c *Collector) SetIngestKey(key string) { + c.ingestKey = key +} + // ListIssues returns deduplicated error issues, most recently seen first. func (c *Collector) ListIssues(limit int) []issues.Issue { return c.issues.List(limit) diff --git a/internal/collector/collector_test.go b/internal/collector/collector_test.go index a1a2d3f..08d710d 100644 --- a/internal/collector/collector_test.go +++ b/internal/collector/collector_test.go @@ -1,6 +1,7 @@ package collector import ( + "bytes" "context" "encoding/json" "net/http" @@ -318,3 +319,61 @@ func TestHandleIssuesAndByID(t *testing.T) { t.Fatalf("expected 404 for unknown issue id, got %d", rec.Code) } } + +func TestHandleLogsIngestKey(t *testing.T) { + coll := New(noopBroadcaster{}, 10, nil) + coll.SetIngestKey("secret") + + body := []byte(`{"resourceLogs":[]}`) + + cases := []struct { + name string + url string + header string + wantStatus int + }{ + {"no key", "/v1/logs", "", http.StatusUnauthorized}, + {"wrong key", "/v1/logs?key=wrong", "", http.StatusUnauthorized}, + {"correct key in query", "/v1/logs?key=secret", "", http.StatusOK}, + {"correct key in header", "/v1/logs", "secret", http.StatusOK}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, tc.url, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + if tc.header != "" { + req.Header.Set("X-Otel-Ingest-Key", tc.header) + } + // Shared IP across subtests; the ingest limiter's burst of 50 is + // well above this test's request count. + req.RemoteAddr = "198.51.100.1:1234" + rec := httptest.NewRecorder() + coll.HandleLogs(rec, req) + if rec.Code != tc.wantStatus { + t.Errorf("expected status %d, got %d (body=%s)", tc.wantStatus, rec.Code, rec.Body.String()) + } + }) + } +} + +func TestHandleLogsIngestRateLimited(t *testing.T) { + coll := New(noopBroadcaster{}, 10, nil) + body := []byte(`{"resourceLogs":[]}`) + + var lastCode int + for i := 0; i < 80; i++ { + req := httptest.NewRequest(http.MethodPost, "/v1/logs", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.RemoteAddr = "198.51.100.2:1234" + rec := httptest.NewRecorder() + coll.HandleLogs(rec, req) + lastCode = rec.Code + if lastCode == http.StatusTooManyRequests { + break + } + } + if lastCode != http.StatusTooManyRequests { + t.Fatalf("expected ingest rate limiting to eventually return 429, last code was %d", lastCode) + } +} diff --git a/internal/collector/handlers.go b/internal/collector/handlers.go index 6e22d37..2210a15 100644 --- a/internal/collector/handlers.go +++ b/internal/collector/handlers.go @@ -19,6 +19,28 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) +// checkIngest applies the shared ingest rate limit and, if configured, the +// ingest key check. It writes an error response and returns false when the +// request should be rejected. +func (c *Collector) checkIngest(w http.ResponseWriter, r *http.Request) bool { + if !c.ingestLimiter.Allow(clientIP(r)) { + http.Error(w, "too many requests, slow down", http.StatusTooManyRequests) + return false + } + if c.ingestKey == "" { + return true + } + key := r.Header.Get("X-Otel-Ingest-Key") + if key == "" { + key = r.URL.Query().Get("key") + } + if key != c.ingestKey { + http.Error(w, "invalid or missing ingest key", http.StatusUnauthorized) + return false + } + return true +} + // HandleTraces handles OTLP trace requests. func (c *Collector) HandleTraces(w http.ResponseWriter, r *http.Request) { log.Printf("Received traces request: method=%s, content-type=%s", r.Method, r.Header.Get("Content-Type")) @@ -26,6 +48,9 @@ func (c *Collector) HandleTraces(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !c.checkIngest(w, r) { + return + } body, err := io.ReadAll(r.Body) if err != nil { @@ -71,6 +96,9 @@ func (c *Collector) HandleMetrics(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !c.checkIngest(w, r) { + return + } body, err := io.ReadAll(r.Body) if err != nil { @@ -112,6 +140,9 @@ func (c *Collector) HandleLogs(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !c.checkIngest(w, r) { + return + } body, err := io.ReadAll(r.Body) if err != nil { diff --git a/static/otel-rum.js b/static/otel-rum.js new file mode 100644 index 0000000..d2aebaf --- /dev/null +++ b/static/otel-rum.js @@ -0,0 +1,277 @@ +/*! + * otel-rum.js - minimal browser error/RUM collector for go-otel. + * + * Captures uncaught errors, unhandled promise rejections and failed + * resource loads, and ships them as OTLP log records to this server's + * existing /v1/logs endpoint (no separate ingestion path needed - the + * backend's issue view picks these up like any other error log). + * + * Also exposes a `traceHeaders()` helper so outgoing `fetch`/XHR calls can + * carry a W3C `traceparent` header. If the backend service is itself + * OTel-instrumented and honors incoming trace context (most server + * instrumentation does by default), spans it creates for that request will + * share the same trace id - so a frontend error and the backend spans it + * triggered can be found together via GET /api/traces/{traceId}. + * + * Usage: + * + * + * + * Explicitly out of scope for v1: source map resolution (stack traces stay + * minified), session replay, PII scrubbing. Don't route sensitive data + * through breadcrumbs/attributes. + */ +(function (global) { + 'use strict'; + + function randomHex(len) { + 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) { + try { + var resolved = new global.URL(url, global.location.href); + return resolved.origin === global.location.origin; + } catch (e) { + return false; + } + } + + function toOtlpAttributes(obj) { + 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) { + config = config || {}; + + var endpoint = config.endpoint || '/v1/logs'; + var serviceName = config.serviceName || 'frontend'; + var environment = config.environment || ''; + var release = config.release || ''; + var ingestKey = config.ingestKey || ''; + 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 injectTraceHeaders = config.injectTraceHeaders !== false; + var traceHeaderOrigins = config.traceHeaderOrigins; // optional array; defaults to same-origin only + + var breadcrumbs = []; + // 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 = ''; + + function addBreadcrumb(category, message) { + breadcrumbs.push({ t: Date.now(), category: String(category), message: String(message) }); + if (breadcrumbs.length > maxBreadcrumbs) { + breadcrumbs.shift(); + } + } + + // 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) { + var traceId = existingTraceId || newTraceId(); + var spanId = newSpanId(); + lastTraceId = traceId; + return { traceparent: '00-' + traceId + '-' + spanId + '-01' }; + } + + function send(severity, message, attrs, traceId) { + if (sampleRate < 1 && Math.random() > sampleRate) return; + + var record = { + timeUnixNano: String(Date.now() * 1e6), + severityText: severity, + severityNumber: severity === 'FATAL' ? 21 : 17, + body: { stringValue: String(message).slice(0, 2000) }, + attributes: toOtlpAttributes( + Object.assign( + { + source: 'frontend', + 'browser.url': global.location ? global.location.href : '', + 'browser.userAgent': global.navigator ? global.navigator.userAgent : '', + 'service.release': release, + 'service.environment': environment, + breadcrumbs: breadcrumbs.length ? JSON.stringify(breadcrumbs) : undefined + }, + attrs || {} + ) + ) + }; + if (traceId) { + record.traceId = traceId; + } + + var payload = { + resourceLogs: [ + { + resource: { attributes: toOtlpAttributes({ 'service.name': serviceName }) }, + scopeLogs: [{ scope: { name: 'otel-rum' }, logRecords: [record] }] + } + ] + }; + + var body = JSON.stringify(payload); + var url = endpoint; + if (ingestKey) { + url += (url.indexOf('?') === -1 ? '?' : '&') + 'key=' + encodeURIComponent(ingestKey); + } + + try { + if (global.navigator && typeof global.navigator.sendBeacon === 'function') { + var blob = new global.Blob([body], { type: 'application/json' }); + if (global.navigator.sendBeacon(url, blob)) return; + } + } catch (e) { + // fall through to fetch + } + if (typeof global.fetch === 'function') { + global.fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body, keepalive: true }).catch(function () {}); + } + } + + function captureException(err, extra) { + var message = (err && err.message) || String(err); + send( + 'ERROR', + message, + Object.assign( + { + 'exception.type': (err && err.name) || 'Error', + 'exception.message': message, + 'exception.stacktrace': (err && err.stack) || '' + }, + extra || {} + ), + lastTraceId + ); + } + + function captureMessage(message, severity, extra) { + send(severity || 'ERROR', message, extra, lastTraceId); + } + + function onError(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; + if (target && target !== global && target.tagName) { + var tag = String(target.tagName).toLowerCase(); + if (tag === 'img' || tag === 'script' || tag === 'link') { + var url = target.src || target.href || ''; + captureMessage('Failed to load ' + tag + ': ' + url, 'ERROR', { + 'exception.type': 'ResourceLoadError', + 'resource.tag': tag, + 'resource.url': url + }); + } + return; + } + + var message = event.message || 'Script error'; + captureException(event.error || { name: 'Error', message: message }, { + 'source.file': event.filename || '', + 'source.line': event.lineno, + 'source.column': event.colno + }); + } + + function onUnhandledRejection(event) { + 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 () { + try { + addBreadcrumb('console.error', Array.prototype.slice.call(arguments).join(' ')); + } catch (e) { + /* ignore */ + } + return originalConsoleError.apply(global.console, arguments); + }; + } + + if (autoInstrumentFetch && typeof global.fetch === 'function') { + var originalFetch = global.fetch; + global.fetch = function (input, requestInit) { + var url = typeof input === 'string' ? input : (input && input.url) || ''; + var method = (requestInit && requestInit.method) || (input && input.method) || 'GET'; + + if (injectTraceHeaders) { + var allowed = traceHeaderOrigins ? traceHeaderOrigins.some(function (o) { return url.indexOf(o) === 0; }) : isSameOrigin(url); + if (allowed) { + requestInit = requestInit || {}; + var headers = new global.Headers(requestInit.headers || (input && input.headers) || {}); + var th = traceHeaders(); + headers.set('traceparent', th.traceparent); + requestInit = Object.assign({}, requestInit, { headers: headers }); + } + } + + return originalFetch.call(global, input, requestInit).then( + function (res) { + addBreadcrumb('fetch', method + ' ' + url + ' -> ' + res.status); + return res; + }, + function (err) { + addBreadcrumb('fetch', method + ' ' + url + ' -> error'); + throw err; + } + ); + }; + } + + return { + captureException: captureException, + captureMessage: captureMessage, + addBreadcrumb: addBreadcrumb, + newTraceId: newTraceId, + newSpanId: newSpanId, + traceHeaders: traceHeaders + }; + } + + global.OtelRUM = { init: init, newTraceId: newTraceId, newSpanId: newSpanId }; +})(typeof window !== 'undefined' ? window : this);