diff --git a/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.test.ts b/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.test.ts index ffffc789c..0a4a13ddb 100644 --- a/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.test.ts +++ b/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.test.ts @@ -4,6 +4,7 @@ import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; import type { CLSMetricWithAttribution, + FCPMetricWithAttribution, MetricWithAttribution, } from 'web-vitals/attribution'; import { @@ -178,7 +179,7 @@ describe('WebVitalsInstrumentation', () => { expect(clsEvent.time).to.deep.equal([3, 0]); }); - it('should not report FCP metrics by default', () => { + it('should report FCP metrics', () => { instrumentation = new WebVitalsInstrumentation({ diag, perf, @@ -186,19 +187,7 @@ describe('WebVitalsInstrumentation', () => { listeners: mockWebVitalListeners, }); - void expect(fcpStub.called).to.be.false; - }); - - it('should report FCP metrics when tracking is set to all', () => { - instrumentation = new WebVitalsInstrumentation({ - diag, - perf, - urlDocument, - trackingLevel: 'all', - listeners: mockWebVitalListeners, - }); - - void expect(fcpStub.calledOnce).to.be.true; + void expect(fcpStub.calledTwice).to.be.true; const { args } = fcpStub.callsArg(0); const metricReportFunc = args[0][0] as WebVitalOnReport; @@ -367,27 +356,15 @@ describe('WebVitalsInstrumentation', () => { expect(inpEvent.time).to.deep.equal([19, 0]); }); - it('should not report TTFB metrics by default', () => { - instrumentation = new WebVitalsInstrumentation({ - diag, - perf, - urlDocument, - listeners: mockWebVitalListeners, - }); - - void expect(ttfbStub.called).to.be.false; - }); - - it('should report TTFB metrics when tracking is set to all', () => { + it('should report TTFB metrics', () => { instrumentation = new WebVitalsInstrumentation({ diag, perf, urlDocument, - trackingLevel: 'all', listeners: mockWebVitalListeners, }); - void expect(ttfbStub.calledOnce).to.be.true; + void expect(ttfbStub.calledTwice).to.be.true; const { args } = ttfbStub.callsArg(0); const metricReportFunc = args[0][0] as WebVitalOnReport; @@ -735,6 +712,148 @@ describe('WebVitalsInstrumentation', () => { }); }); + it('should attribute the correct URL for FCP metrics', () => { + const testDocument: URLDocument = { + URL: 'https://first.com', + }; + const pageManager = new EmbracePageManager(); + pageManager.setCurrentRoute({ + path: '/first/:id', + url: '/first/123', + }); + instrumentation = new WebVitalsInstrumentation({ + diag, + perf, + pageManager, + urlDocument: testDocument, + listeners: mockWebVitalListeners, + urlAttribution: true, + }); + + void expect(fcpStub.callCount).to.equal(2); + const fcpFinalReportFunc = fcpStub.getCall(0).args[0] as WebVitalOnReport; + const fcpChangeReportFunc = fcpStub.getCall(1).args[0] as WebVitalOnReport; + + const fcpMetric = { + name: 'FCP', + value: 22, + rating: 'poor', + delta: 0, + id: 'm1', + entries: [], + navigationType: 'navigate', + attribution: { + timeToFirstByte: 0, + firstByteToFCP: 0, + loadState: 'complete', + }, + } as FCPMetricWithAttribution; + + fcpChangeReportFunc(fcpMetric); + + testDocument.URL = 'https://second.com'; + pageManager.setCurrentRoute({ + path: '/second/:id', + url: '/second/123', + }); + const attributedPageID = pageManager.getCurrentPageId(); + fcpChangeReportFunc(fcpMetric); + // should NOT be attributed to this URL since the metric hasn't changed + testDocument.URL = 'https://third.com'; + pageManager.setCurrentRoute({ + path: '/third/:id', + url: '/third/123', + }); + fcpFinalReportFunc(fcpMetric); + + spanSessionManager.endSessionSpan(); + const finishedSpans = memoryExporter.getFinishedSpans(); + expect(finishedSpans).to.have.lengthOf(1); + const sessionSpan = finishedSpans[0]; + expect(sessionSpan.events).to.have.lengthOf(1); + + const fcpEvent = sessionSpan.events[0]; + + expect(fcpEvent.name).to.be.equal('emb-web-vitals-report-FCP'); + expect(fcpEvent.attributes).to.containSubset({ + 'url.full': 'https://second.com', + 'app.surface.name': '/second/:id', + 'app.surface.id': attributedPageID, + }); + }); + + it('should attribute the correct URL for TTFB metrics', () => { + const testDocument: URLDocument = { + URL: 'https://first.com', + }; + const pageManager = new EmbracePageManager(); + pageManager.setCurrentRoute({ + path: '/first/:id', + url: '/first/123', + }); + instrumentation = new WebVitalsInstrumentation({ + diag, + perf, + pageManager, + urlDocument: testDocument, + listeners: mockWebVitalListeners, + urlAttribution: true, + }); + + void expect(ttfbStub.callCount).to.equal(2); + const ttfbFinalReportFunc = ttfbStub.getCall(0).args[0] as WebVitalOnReport; + const ttfbChangeReportFunc = ttfbStub.getCall(1) + .args[0] as WebVitalOnReport; + + const ttfbMetric = { + name: 'TTFB', + value: 33, + rating: 'poor', + delta: 99, + id: 'm1', + entries: [], + navigationType: 'navigate', + attribution: { + waitingDuration: 20, + cacheDuration: 40, + dnsDuration: 60, + connectionDuration: 80, + requestDuration: 100, + }, + } as MetricWithAttribution; + + ttfbChangeReportFunc(ttfbMetric); + // should be attributed to this URL since that is when the last change to the metric occurred + testDocument.URL = 'https://second.com'; + pageManager.setCurrentRoute({ + path: '/second/:id', + url: '/second/123', + }); + const attributedPageID = pageManager.getCurrentPageId(); + ttfbChangeReportFunc(ttfbMetric); + testDocument.URL = 'https://third.com'; + pageManager.setCurrentRoute({ + path: '/third/:id', + url: '/third/123', + }); + ttfbFinalReportFunc(ttfbMetric); + + spanSessionManager.endSessionSpan(); + const finishedSpans = memoryExporter.getFinishedSpans(); + expect(finishedSpans).to.have.lengthOf(1); + const sessionSpan = finishedSpans[0]; + expect(sessionSpan.events).to.have.lengthOf(1); + + const ttfbEvent = sessionSpan.events[0]; + + expect(ttfbEvent.name).to.be.equal('emb-web-vitals-report-TTFB'); + expect(ttfbEvent.attributes).to.containSubset({ + 'url.full': 'https://second.com', + 'app.surface.name': '/second/:id', + 'app.surface.id': attributedPageID, + }); + }); + it('should attach page attributes when route is set', () => { const pageManager = new EmbracePageManager(); pageManager.setCurrentRoute({ diff --git a/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.ts b/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.ts index 532423813..c1ed98d68 100644 --- a/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.ts +++ b/packages/web-sdk/src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.ts @@ -20,7 +20,6 @@ import { import { EmbraceInstrumentationBase } from '../../EmbraceInstrumentationBase/index.ts'; import { ALL_WEB_VITALS, - CORE_WEB_VITALS, EMB_WEB_VITALS_PREFIX, WEB_VITALS_ID_TO_LISTENER, } from './constants.ts'; @@ -134,7 +133,6 @@ export class WebVitalsInstrumentation extends EmbraceInstrumentationBase { public constructor({ diag, perf, - trackingLevel = 'core', listeners = WEB_VITALS_ID_TO_LISTENER, urlDocument = window.document, urlAttribution = true, @@ -149,8 +147,7 @@ export class WebVitalsInstrumentation extends EmbraceInstrumentationBase { }); this._listeners = listeners; this._urlDocument = urlDocument; - this._metricsToTrack = - trackingLevel === 'all' ? [...ALL_WEB_VITALS] : [...CORE_WEB_VITALS]; + this._metricsToTrack = [...ALL_WEB_VITALS]; this._urlAttribution = urlAttribution; this._pageManager = pageManager ?? page.getPageManager(); @@ -214,6 +211,22 @@ export class WebVitalsInstrumentation extends EmbraceInstrumentationBase { // When these web vitals make their final report (e.g. when the listeners w/ reportAllChanges=false trigger) the // document's URL at that time may not match what it was at the time the scores were last updated. Instead, listen // for updates to the scores and keep track of the Page information to attribute for each + this._listeners.TTFB?.( + () => { + this._attributedPage.TTFB = this._currentAttributedPage(); + }, + { + reportAllChanges: true, + }, + ); + this._listeners.FCP?.( + () => { + this._attributedPage.FCP = this._currentAttributedPage(); + }, + { + reportAllChanges: true, + }, + ); this._listeners.INP?.( () => { this._attributedPage.INP = this._currentAttributedPage(); @@ -286,6 +299,14 @@ export class WebVitalsInstrumentation extends EmbraceInstrumentationBase { private _getAttributedPageForMetric( metric: MetricWithAttribution, ): AttributedPage { + if (metric.name === 'FCP' && this._attributedPage.FCP) { + return this._attributedPage.FCP; + } + + if (metric.name === 'TTFB' && this._attributedPage.TTFB) { + return this._attributedPage.TTFB; + } + if (metric.name === 'INP' && this._attributedPage.INP) { return this._attributedPage.INP; }