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.
This commit is contained in:
arielbackenroth
2025-10-27 14:35:13 +00:00
committed by Kristiyan Kostadinov
parent 763db3a70b
commit 07b8e953f4
3 changed files with 35 additions and 12 deletions
+21 -10
View File
@@ -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<any>) {
*/
@Injectable({providedIn: 'root'})
export class HttpXhrBackend implements HttpBackend {
private readonly tracingService: TracingService<TracingSnapshot> | null = inject(TracingService, {
optional: true,
});
constructor(private xhrFactory: XhrFactory) {}
private maybePropagateTrace<T extends Function>(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);
+7 -2
View File
@@ -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));
+7
View File
@@ -49,6 +49,13 @@ export interface TracingService<T extends TracingSnapshot> {
*/
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?<T extends Function>(fn: T): T;
/**
* Wrap an event listener bound by the framework for tracing.
* @param element Element on which the event is bound.