Skip to content

Commit 6151a9b

Browse files
authored
fix: resolve npe in web vitals attribution (#716)
1 parent 8377316 commit 6151a9b

File tree

2 files changed

+110
-24
lines changed

2 files changed

+110
-24
lines changed

src/plugins/event-plugins/WebVitalsPlugin.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,22 @@ export class WebVitalsPlugin extends InternalPlugin {
9191
private handleLCP(metric: LCPMetricWithAttribution | Metric) {
9292
const a = (metric as LCPMetricWithAttribution).attribution;
9393
const attribution: RumLCPAttribution = {
94-
element: a.element,
95-
url: a.url,
96-
timeToFirstByte: a.timeToFirstByte,
97-
resourceLoadDelay: a.resourceLoadDelay,
94+
element: a?.element,
95+
url: a?.url,
96+
timeToFirstByte: a?.timeToFirstByte,
97+
resourceLoadDelay: a?.resourceLoadDelay,
9898
/**
9999
* `resourceLoadTime` was renamed to `resourceLoadDuration` in web-vitals 4.x
100100
* This is a cosmetic change, and does not affect the underlying value.
101101
* We can update the name to `resourceLoadDuration` in RUM's next major version.
102102
* (See https://github.com/GoogleChrome/web-vitals/pull/450)
103103
*/
104-
resourceLoadTime: a.resourceLoadDuration,
104+
resourceLoadTime: a?.resourceLoadDuration,
105105
// See https://github.com/GoogleChrome/web-vitals/pull/450
106-
elementRenderDelay: a.elementRenderDelay
106+
elementRenderDelay: a?.elementRenderDelay
107107
};
108-
if (a.lcpResourceEntry) {
109-
const key = performanceKey(a.lcpResourceEntry as HasLatency);
108+
if (a?.lcpResourceEntry) {
109+
const key = performanceKey(a?.lcpResourceEntry as HasLatency);
110110
attribution.lcpResourceEntry = this.resourceEventIds.get(key);
111111
}
112112
if (this.navigationEventId) {
@@ -131,10 +131,10 @@ export class WebVitalsPlugin extends InternalPlugin {
131131
version: '1.0.0',
132132
value: metric.value,
133133
attribution: {
134-
largestShiftTarget: a.largestShiftTarget,
135-
largestShiftValue: a.largestShiftValue,
136-
largestShiftTime: a.largestShiftTime,
137-
loadState: a.loadState
134+
largestShiftTarget: a?.largestShiftTarget,
135+
largestShiftValue: a?.largestShiftValue,
136+
largestShiftTime: a?.largestShiftTime,
137+
loadState: a?.loadState
138138
}
139139
} as CumulativeLayoutShiftEvent);
140140
}
@@ -145,10 +145,10 @@ export class WebVitalsPlugin extends InternalPlugin {
145145
version: '1.0.0',
146146
value: metric.value,
147147
attribution: {
148-
eventTarget: a.eventTarget,
149-
eventType: a.eventType,
150-
eventTime: a.eventTime,
151-
loadState: a.loadState
148+
eventTarget: a?.eventTarget,
149+
eventType: a?.eventType,
150+
eventTime: a?.eventTime,
151+
loadState: a?.loadState
152152
}
153153
} as FirstInputDelayEvent);
154154
}
@@ -160,14 +160,14 @@ export class WebVitalsPlugin extends InternalPlugin {
160160
version: '1.0.0',
161161
value: metric.value,
162162
attribution: {
163-
interactionTarget: a.interactionTarget,
164-
interactionTime: a.interactionTime,
165-
nextPaintTime: a.nextPaintTime,
166-
interactionType: a.interactionType,
167-
inputDelay: a.inputDelay,
168-
processingDuration: a.processingDuration,
169-
presentationDelay: a.presentationDelay,
170-
loadState: a.loadState
163+
interactionTarget: a?.interactionTarget,
164+
interactionTime: a?.interactionTime,
165+
nextPaintTime: a?.nextPaintTime,
166+
interactionType: a?.interactionType,
167+
inputDelay: a?.inputDelay,
168+
processingDuration: a?.processingDuration,
169+
presentationDelay: a?.presentationDelay,
170+
loadState: a?.loadState
171171
}
172172
} as InteractionToNextPaintEvent);
173173
}

src/plugins/event-plugins/__tests__/WebVitalsPlugin.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,4 +453,90 @@ describe('WebVitalsPlugin tests', () => {
453453
expect.anything()
454454
);
455455
});
456+
457+
test('When web vitals have null attribution then they handle gracefully', async () => {
458+
// eslint-disable-next-line @typescript-eslint/no-var-requires
459+
const webVitals = require('web-vitals/attribution');
460+
461+
// Test LCP with null attribution
462+
webVitals.onLCP.mockImplementationOnce((callback) => {
463+
callback({ ...mockLCPData, attribution: null });
464+
});
465+
466+
// Test FID with null attribution
467+
webVitals.onFID.mockImplementationOnce((callback) => {
468+
callback({ ...mockFIDData, attribution: null });
469+
});
470+
471+
// Test CLS with null attribution
472+
webVitals.onCLS.mockImplementationOnce((callback) => {
473+
callback({ ...mockCLSData, attribution: null });
474+
});
475+
476+
// Test INP with null attribution
477+
webVitals.onINP.mockImplementationOnce((callback) => {
478+
callback({ ...mockINPData, attribution: null });
479+
});
480+
481+
const plugin = new WebVitalsPlugin();
482+
plugin.load(context);
483+
484+
// Verify LCP handles null attribution
485+
expect(record).toHaveBeenCalledWith(
486+
LCP_EVENT_TYPE,
487+
expect.objectContaining({
488+
attribution: expect.objectContaining({
489+
element: undefined,
490+
url: undefined,
491+
timeToFirstByte: undefined,
492+
resourceLoadDelay: undefined,
493+
resourceLoadTime: undefined,
494+
elementRenderDelay: undefined
495+
})
496+
})
497+
);
498+
499+
// Verify FID handles null attribution
500+
expect(record).toHaveBeenCalledWith(
501+
FID_EVENT_TYPE,
502+
expect.objectContaining({
503+
attribution: expect.objectContaining({
504+
eventTarget: undefined,
505+
eventType: undefined,
506+
eventTime: undefined,
507+
loadState: undefined
508+
})
509+
})
510+
);
511+
512+
// Verify CLS handles null attribution
513+
expect(recordCandidate).toHaveBeenCalledWith(
514+
CLS_EVENT_TYPE,
515+
expect.objectContaining({
516+
attribution: expect.objectContaining({
517+
largestShiftTarget: undefined,
518+
largestShiftValue: undefined,
519+
largestShiftTime: undefined,
520+
loadState: undefined
521+
})
522+
})
523+
);
524+
525+
// Verify INP handles null attribution
526+
expect(recordCandidate).toHaveBeenCalledWith(
527+
INP_EVENT_TYPE,
528+
expect.objectContaining({
529+
attribution: expect.objectContaining({
530+
interactionTarget: undefined,
531+
interactionTime: undefined,
532+
nextPaintTime: undefined,
533+
interactionType: undefined,
534+
inputDelay: undefined,
535+
processingDuration: undefined,
536+
presentationDelay: undefined,
537+
loadState: undefined
538+
})
539+
})
540+
);
541+
});
456542
});

0 commit comments

Comments
 (0)