From 07b8e953f46b454fd3e59ecbcffae9b6c1a53a71 Mon Sep 17 00:00:00 2001 From: arielbackenroth Date: Mon, 27 Oct 2025 14:35:13 +0000 Subject: [PATCH] refactor(http): add hooks for propagating traces across XHR callbacks. Enables propagating a trace across XHR callbacks by providing a hook for wrapping the callback with a function bound to the send trace context. --- packages/common/http/src/xhr.ts | 31 ++++++++++++++++-------- packages/common/http/test/xhr_spec.ts | 9 +++++-- packages/core/src/application/tracing.ts | 7 ++++++ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/common/http/src/xhr.ts b/packages/common/http/src/xhr.ts index ff2e603cf66..2e4e50229f5 100644 --- a/packages/common/http/src/xhr.ts +++ b/packages/common/http/src/xhr.ts @@ -8,9 +8,12 @@ import {XhrFactory} from '../../index'; import { + inject, Injectable, ɵRuntimeError as RuntimeError, ɵformatRuntimeError as formatRuntimeError, + ɵTracingService as TracingService, + ɵTracingSnapshot as TracingSnapshot, } from '@angular/core'; import {from, Observable, Observer, of} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -103,8 +106,16 @@ function validateXhrCompatibility(req: HttpRequest) { */ @Injectable({providedIn: 'root'}) export class HttpXhrBackend implements HttpBackend { + private readonly tracingService: TracingService | null = inject(TracingService, { + optional: true, + }); + constructor(private xhrFactory: XhrFactory) {} + private maybePropagateTrace(fn: T): T { + return this.tracingService?.propagate ? this.tracingService.propagate(fn) : fn; + } + /** * Processes a request and returns a stream of response events. * @param req The request object. @@ -219,7 +230,7 @@ export class HttpXhrBackend implements HttpBackend { // emit. This allows them to be unregistered as event listeners later. // First up is the load event, which represents a response being fully available. - const onLoad = () => { + const onLoad = this.maybePropagateTrace(() => { // Read response state from the memoized partial data. let {headers, status, statusText, url} = partialFromXhr(); @@ -296,12 +307,12 @@ export class HttpXhrBackend implements HttpBackend { }), ); } - }; + }); // The onError callback is called when something goes wrong at the network level. // Connection timeout, DNS error, offline, etc. These are actual errors, and are // transmitted on the error channel. - const onError = (error: ProgressEvent) => { + const onError = this.maybePropagateTrace((error: ProgressEvent) => { const {url} = partialFromXhr(); const res = new HttpErrorResponse({ error, @@ -310,12 +321,12 @@ export class HttpXhrBackend implements HttpBackend { url: url || undefined, }); observer.error(res); - }; + }); let onTimeout = onError; if (req.timeout) { - onTimeout = (_: ProgressEvent) => { + onTimeout = this.maybePropagateTrace((_: ProgressEvent) => { const {url} = partialFromXhr(); const res = new HttpErrorResponse({ error: new DOMException('Request timed out', 'TimeoutError'), @@ -324,7 +335,7 @@ export class HttpXhrBackend implements HttpBackend { url: url || undefined, }); observer.error(res); - }; + }); } // The sentHeaders flag tracks whether the HttpResponseHeaders event @@ -335,7 +346,7 @@ export class HttpXhrBackend implements HttpBackend { // The download progress event handler, which is only registered if // progress events are enabled. - const onDownProgress = (event: ProgressEvent) => { + const onDownProgress = this.maybePropagateTrace((event: ProgressEvent) => { // Send the HttpResponseHeaders event if it hasn't been sent already. if (!sentHeaders) { observer.next(partialFromXhr()); @@ -363,11 +374,11 @@ export class HttpXhrBackend implements HttpBackend { // Finally, fire the event. observer.next(progressEvent); - }; + }); // The upload progress event handler, which is only registered if // progress events are enabled. - const onUpProgress = (event: ProgressEvent) => { + const onUpProgress = this.maybePropagateTrace((event: ProgressEvent) => { // Upload progress events are simpler. Begin building the progress // event. let progress: HttpUploadProgressEvent = { @@ -383,7 +394,7 @@ export class HttpXhrBackend implements HttpBackend { // Send the event. observer.next(progress); - }; + }); // By default, register for load and error events. xhr.addEventListener('load', onLoad); diff --git a/packages/common/http/test/xhr_spec.ts b/packages/common/http/test/xhr_spec.ts index 882c75f0e29..23dc99320e7 100644 --- a/packages/common/http/test/xhr_spec.ts +++ b/packages/common/http/test/xhr_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import {XhrFactory} from '../../index'; import {HttpRequest} from '../src/request'; import { HttpDownloadProgressEvent, @@ -19,6 +20,7 @@ import { HttpUploadProgressEvent, } from '../src/response'; import {HttpXhrBackend} from '../src/xhr'; +import {TestBed} from '@angular/core/testing'; import {Observable} from 'rxjs'; import {toArray} from 'rxjs/operators'; @@ -53,8 +55,11 @@ describe('XhrBackend', () => { let factory: MockXhrFactory = null!; let backend: HttpXhrBackend = null!; beforeEach(() => { - factory = new MockXhrFactory(); - backend = new HttpXhrBackend(factory); + TestBed.configureTestingModule({ + providers: [{provide: XhrFactory, useClass: MockXhrFactory}], + }); + factory = TestBed.inject(XhrFactory) as MockXhrFactory; + backend = TestBed.inject(HttpXhrBackend); }); it('emits status immediately', () => { const events = trackEvents(backend.handle(TEST_POST)); diff --git a/packages/core/src/application/tracing.ts b/packages/core/src/application/tracing.ts index 1dd12148c40..d241171c22c 100644 --- a/packages/core/src/application/tracing.ts +++ b/packages/core/src/application/tracing.ts @@ -49,6 +49,13 @@ export interface TracingService { */ snapshot(linkedSnapshot: T | null): T; + /** + * Propagate the current tracing context to the provided function. + * @param fn A function. + * @return A function that will propagate the current tracing context. + */ + propagate?(fn: T): T; + /** * Wrap an event listener bound by the framework for tracing. * @param element Element on which the event is bound.