From ca848215b9fbc981897ca14202ceca006f0c5f84 Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Thu, 26 Sep 2024 16:13:51 -0400 Subject: [PATCH 1/8] Add data attributtes for LCP. --- .../src/web-vitals-autoinstrumentation.ts | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts index dd0274d1..d0766b7a 100644 --- a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts +++ b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts @@ -88,7 +88,14 @@ interface VitalOpts extends ReportOpts { applyCustomAttributes?: ApplyCustomAttributesFn; } -interface VitalOptsWithTimings extends VitalOpts { +interface LcpVitalOpts extends VitalOpts { + /** + * Will send the values of these data attributes if they appear on an LCP event + */ + dataAttributes: string[]; +} + +interface InpVitalOpts extends VitalOpts { /** * if this is true it will create spans from the PerformanceLongAnimationFrameTiming frames */ @@ -210,13 +217,13 @@ export interface WebVitalsInstrumentationConfig extends InstrumentationConfig { vitalsToTrack?: Array; /** Config specific to LCP (Largest Contentful Paint) */ - lcp?: VitalOpts; + lcp?: LcpVitalOpts; /** Config specific to CLS (Cumulative Layout Shift) */ cls?: VitalOpts; /** Config specific to INP (Interaction to Next Paint) */ - inp?: VitalOptsWithTimings; + inp?: InpVitalOpts; /** Config specific to FID (First Input Delay) */ fid?: VitalOpts; @@ -235,9 +242,9 @@ export interface WebVitalsInstrumentationConfig extends InstrumentationConfig { */ export class WebVitalsInstrumentation extends InstrumentationAbstract { readonly vitalsToTrack: Array; - readonly lcpOpts?: VitalOpts; + readonly lcpOpts?: LcpVitalOpts; readonly clsOpts?: VitalOpts; - readonly inpOpts?: VitalOptsWithTimings; + readonly inpOpts?: InpVitalOpts; readonly fidOpts?: VitalOpts; readonly fcpOpts?: VitalOpts; readonly ttfbOpts?: VitalOpts; @@ -280,41 +287,37 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { private _setupWebVitalsCallbacks() { if (this.vitalsToTrack.includes('CLS')) { onCLS((vital) => { - this.onReportCLS(vital, this.clsOpts?.applyCustomAttributes); + this.onReportCLS(vital, this.clsOpts); }, this.clsOpts); } if (this.vitalsToTrack.includes('LCP')) { onLCP((vital) => { - this.onReportLCP(vital, this.lcpOpts?.applyCustomAttributes); + this.onReportLCP(vital, this.lcpOpts); }, this.lcpOpts); } if (this.vitalsToTrack.includes('INP')) { onINP((vital) => { - this.onReportINP( - vital, - this.inpOpts?.applyCustomAttributes, - this.inpOpts?.includeTimingsAsSpans, - ); + this.onReportINP(vital, this.inpOpts); }, this.inpOpts); } if (this.vitalsToTrack.includes('FID')) { onFID((vital) => { - this.onReportFID(vital, this.fidOpts?.applyCustomAttributes); + this.onReportFID(vital, this.fidOpts); }, this.fidOpts); } if (this.vitalsToTrack.includes('TTFB')) { onTTFB((vital) => { - this.onReportTTFB(vital, this.ttfbOpts?.applyCustomAttributes); + this.onReportTTFB(vital, this.ttfbOpts); }, this.ttfbOpts); } if (this.vitalsToTrack.includes('FCP')) { onFCP((vital) => { - this.onReportFCP(vital, this.fcpOpts?.applyCustomAttributes); + this.onReportFCP(vital, this.fcpOpts); }, this.fcpOpts); } } @@ -417,10 +420,8 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { }); } - onReportCLS = ( - cls: CLSMetricWithAttribution, - applyCustomAttributes?: ApplyCustomAttributesFn, - ) => { + onReportCLS = (cls: CLSMetricWithAttribution, clsOpts: VitalOpts = {}) => { + const { applyCustomAttributes } = clsOpts; if (!this.isEnabled()) return; const { name, attribution } = cls; @@ -453,8 +454,9 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { onReportLCP = ( lcp: LCPMetricWithAttribution, - applyCustomAttributes?: ApplyCustomAttributesFn, + lcpOpts: LcpVitalOpts = { dataAttributes: [] }, ) => { + const { applyCustomAttributes, dataAttributes } = lcpOpts; if (!this.isEnabled()) return; const { name, attribution } = lcp; @@ -465,6 +467,7 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { resourceLoadDelay, resourceLoadDuration, elementRenderDelay, + lcpEntry, }: LCPAttribution = attribution; const attrPrefix = this.getAttrPrefix(name); @@ -481,6 +484,15 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { [`${attrPrefix}.resource_load_time`]: resourceLoadDuration, }); + if (dataAttributes.length >= 0) { + dataAttributes.forEach((dataAttr) => { + const value = lcpEntry?.element?.getAttribute(dataAttr); + if (value !== null && value !== undefined) { + span.setAttribute(`${attrPrefix}.element.${dataAttr}`, value); + } + }); + } + if (applyCustomAttributes) { applyCustomAttributes(lcp, span); } @@ -490,9 +502,9 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { onReportINP = ( inp: INPMetricWithAttribution, - applyCustomAttributes?: ApplyCustomAttributesFn, - includeTimingsAsSpans = false, + inpOpts: InpVitalOpts = { includeTimingsAsSpans: false }, ) => { + const { applyCustomAttributes, includeTimingsAsSpans } = inpOpts; if (!this.isEnabled()) return; const { name, attribution } = inp; @@ -550,10 +562,8 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { ); }; - onReportFCP = ( - fcp: FCPMetricWithAttribution, - applyCustomAttributes?: ApplyCustomAttributesFn, - ) => { + onReportFCP = (fcp: FCPMetricWithAttribution, fcpOpts: VitalOpts = {}) => { + const { applyCustomAttributes } = fcpOpts; if (!this.isEnabled()) return; const { name, attribution } = fcp; @@ -580,10 +590,8 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { /** * @deprecated this will be removed in the next major version, use INP instead. */ - onReportFID = ( - fid: FIDMetricWithAttribution, - applyCustomAttributes?: ApplyCustomAttributesFn, - ) => { + onReportFID = (fid: FIDMetricWithAttribution, fidOpts: VitalOpts = {}) => { + const { applyCustomAttributes } = fidOpts; if (!this.isEnabled()) return; const { name, attribution } = fid; @@ -608,8 +616,9 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { onReportTTFB = ( ttfb: TTFBMetricWithAttribution, - applyCustomAttributes?: ApplyCustomAttributesFn, + ttfbOpts: VitalOpts = {}, ) => { + const { applyCustomAttributes } = ttfbOpts; if (!this.isEnabled()) return; const { name, attribution } = ttfb; From 56a9c13f394f7b559d32cbd15ad980d206bfa15a Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Thu, 26 Sep 2024 16:18:41 -0400 Subject: [PATCH 2/8] Update test. --- .../src/web-vitals-autoinstrumentation.ts | 8 +- .../test/web-vitals-instrumentation.test.ts | 107 ++++++++++-------- 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts index d0766b7a..28ab6504 100644 --- a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts +++ b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts @@ -92,14 +92,14 @@ interface LcpVitalOpts extends VitalOpts { /** * Will send the values of these data attributes if they appear on an LCP event */ - dataAttributes: string[]; + dataAttributes?: string[]; } interface InpVitalOpts extends VitalOpts { /** * if this is true it will create spans from the PerformanceLongAnimationFrameTiming frames */ - includeTimingsAsSpans: boolean; + includeTimingsAsSpans?: boolean; } // To avoid importing InstrumentationAbstract from: @@ -484,8 +484,8 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { [`${attrPrefix}.resource_load_time`]: resourceLoadDuration, }); - if (dataAttributes.length >= 0) { - dataAttributes.forEach((dataAttr) => { + if (dataAttributes && dataAttributes.length >= 0) { + dataAttributes?.forEach((dataAttr) => { const value = lcpEntry?.element?.getAttribute(dataAttr); if (value !== null && value !== undefined) { span.setAttribute(`${attrPrefix}.element.${dataAttr}`, value); diff --git a/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts b/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts index 35529bf3..27b9f9d1 100644 --- a/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts +++ b/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts @@ -303,11 +303,13 @@ describe('Web Vitals Instrumentation Tests', () => { describe('CLS', () => { it('should create a span when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportCLS(CLS, (cls, span) => { - span.setAttributes({ - 'cls.entries': cls.entries.toString(), - 'cls.my_custom_attr': 'custom_attr', - }); + webVitalsInstr.onReportCLS(CLS, { + applyCustomAttributes: (cls, span) => { + span.setAttributes({ + 'cls.entries': cls.entries.toString(), + 'cls.my_custom_attr': 'custom_attr', + }); + }, }); const span = exporter.getFinishedSpans()[0]; @@ -323,11 +325,11 @@ describe('Web Vitals Instrumentation Tests', () => { vitalsToTrack: ['CLS'], }); instr.disable(); - instr.onReportCLS(CLS, () => {}); + instr.onReportCLS(CLS, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(0); instr.enable(); - instr.onReportCLS(CLS, () => {}); + instr.onReportCLS(CLS, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('CLS'); @@ -337,11 +339,14 @@ describe('Web Vitals Instrumentation Tests', () => { describe('LCP', () => { it('should create a span when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportLCP(LCP, (lcp, span) => { - span.setAttributes({ - 'lcp.entries': lcp.entries.toString(), - 'lcp.my_custom_attr': 'custom_attr', - }); + webVitalsInstr.onReportLCP(LCP, { + dataAttributes: [], + applyCustomAttributes: (lcp, span) => { + span.setAttributes({ + 'lcp.entries': lcp.entries.toString(), + 'lcp.my_custom_attr': 'custom_attr', + }); + }, }); const span = exporter.getFinishedSpans()[0]; @@ -357,11 +362,11 @@ describe('Web Vitals Instrumentation Tests', () => { vitalsToTrack: ['LCP'], }); instr.disable(); - instr.onReportLCP(LCP, () => {}); + instr.onReportLCP(LCP, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(0); instr.enable(); - instr.onReportLCP(LCP, () => {}); + instr.onReportLCP(LCP, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('LCP'); @@ -371,11 +376,13 @@ describe('Web Vitals Instrumentation Tests', () => { describe('INP', () => { it('should create a span when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportINP(INP, (inp, span) => { - span.setAttributes({ - 'inp.entries': inp.entries.toString(), - 'inp.my_custom_attr': 'custom_attr', - }); + webVitalsInstr.onReportINP(INP, { + applyCustomAttributes: (inp, span) => { + span.setAttributes({ + 'inp.entries': inp.entries.toString(), + 'inp.my_custom_attr': 'custom_attr', + }); + }, }); const span = exporter.getFinishedSpans()[0]; @@ -388,20 +395,18 @@ describe('Web Vitals Instrumentation Tests', () => { it('should create a include timings when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportINP( - INPWithTimings, - (inp, span) => { + webVitalsInstr.onReportINP(INPWithTimings, { + applyCustomAttributes: (inp, span) => { span.setAttributes({ 'inp.entries': inp.entries.toString(), 'inp.my_custom_attr': 'custom_attr', }); }, - true, - ); + includeTimingsAsSpans: true, + }); const [scriptTimingSpan, timingSpan, inpSpan] = exporter.getFinishedSpans(); - console.log({ timingSpan }); expect(inpSpan.name).toBe('INP'); expect(inpSpan.instrumentationLibrary.name).toBe( '@honeycombio/instrumentation-web-vitals', @@ -433,11 +438,11 @@ describe('Web Vitals Instrumentation Tests', () => { vitalsToTrack: ['INP'], }); instr.disable(); - instr.onReportINP(INP, () => {}); + instr.onReportINP(INP, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(0); instr.enable(); - instr.onReportINP(INP, () => {}); + instr.onReportINP(INP, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('INP'); @@ -447,11 +452,13 @@ describe('Web Vitals Instrumentation Tests', () => { describe('FCP', () => { it('should create a span when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportFCP(FCP, (fcp, span) => { - span.setAttributes({ - 'fcp.entries': fcp.entries.toString(), - 'fcp.my_custom_attr': 'custom_attr', - }); + webVitalsInstr.onReportFCP(FCP, { + applyCustomAttributes: (fcp, span) => { + span.setAttributes({ + 'fcp.entries': fcp.entries.toString(), + 'fcp.my_custom_attr': 'custom_attr', + }); + }, }); const span = exporter.getFinishedSpans()[0]; expect(span.name).toBe('FCP'); @@ -466,11 +473,11 @@ describe('Web Vitals Instrumentation Tests', () => { vitalsToTrack: ['FCP'], }); instr.disable(); - instr.onReportFCP(FCP, () => {}); + instr.onReportFCP(FCP, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(0); instr.enable(); - instr.onReportFCP(FCP, () => {}); + instr.onReportFCP(FCP, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('FCP'); @@ -480,11 +487,13 @@ describe('Web Vitals Instrumentation Tests', () => { describe('TTFB', () => { it('should create a span when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportTTFB(TTFB, (ttfb, span) => { - span.setAttributes({ - 'ttfb.entries': ttfb.entries.toString(), - 'ttfb.my_custom_attr': 'custom_attr', - }); + webVitalsInstr.onReportTTFB(TTFB, { + applyCustomAttributes: (ttfb, span) => { + span.setAttributes({ + 'ttfb.entries': ttfb.entries.toString(), + 'ttfb.my_custom_attr': 'custom_attr', + }); + }, }); const span = exporter.getFinishedSpans()[0]; @@ -500,11 +509,11 @@ describe('Web Vitals Instrumentation Tests', () => { vitalsToTrack: ['TTFB'], }); instr.disable(); - instr.onReportTTFB(TTFB, () => {}); + instr.onReportTTFB(TTFB, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(0); instr.enable(); - instr.onReportTTFB(TTFB, () => {}); + instr.onReportTTFB(TTFB, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('TTFB'); @@ -514,11 +523,13 @@ describe('Web Vitals Instrumentation Tests', () => { describe('FID', () => { it('should create a span when enabled', () => { const webVitalsInstr = new WebVitalsInstrumentation(); - webVitalsInstr.onReportFID(FID, (fid, span) => { - span.setAttributes({ - 'fid.entries': fid.entries.toString(), - 'fid.my_custom_attr': 'custom_attr', - }); + webVitalsInstr.onReportFID(FID, { + applyCustomAttributes: (fid, span) => { + span.setAttributes({ + 'fid.entries': fid.entries.toString(), + 'fid.my_custom_attr': 'custom_attr', + }); + }, }); const span = exporter.getFinishedSpans()[0]; @@ -534,11 +545,11 @@ describe('Web Vitals Instrumentation Tests', () => { vitalsToTrack: ['FID'], }); instr.disable(); - instr.onReportFID(FID, () => {}); + instr.onReportFID(FID, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(0); instr.enable(); - instr.onReportFID(FID, () => {}); + instr.onReportFID(FID, { applyCustomAttributes: () => {} }); expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('FID'); From 04dc434be61bfabbb8170e912273a22c7531031a Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Thu, 26 Sep 2024 16:36:15 -0400 Subject: [PATCH 3/8] Update example. --- .../examples/hello-world-web/index.html | 38 +++++++++---------- .../examples/hello-world-web/index.js | 3 ++ .../hello-world-web/package-lock.json | 3 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html index 2fe85a28..31fd7ec2 100644 --- a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html +++ b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html @@ -1,23 +1,23 @@ - + - - - - Honeycomb OpenTelemetry Web Distro - - + + + + Honeycomb OpenTelemetry Web Distro + +
    -
    -
    -

    👋 Hello World

    -
    +
    +
    +

    👋 Hello World

    +
    - -
    - -
    -
    - - - + +
    + +
    +
    + + + diff --git a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js index b2395b5b..7d994b2b 100644 --- a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js +++ b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js @@ -26,6 +26,9 @@ const main = () => { contextManager: new ZoneContextManager(), webVitalsInstrumentationConfig: { vitalsToTrack: ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'], + lcp: { + dataAttributes: ['data-hello'], + }, }, }); sdk.start(); diff --git a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/package-lock.json b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/package-lock.json index 81a2988b..fc1b0c27 100644 --- a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/package-lock.json +++ b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/package-lock.json @@ -22,11 +22,12 @@ }, "../..": { "name": "@honeycombio/opentelemetry-web", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.24.7", "@opentelemetry/api": "~1.9.0", + "@opentelemetry/auto-instrumentations-web": "^0.41.0", "@opentelemetry/core": "~1.25.1", "@opentelemetry/exporter-trace-otlp-http": "~0.52.1", "@opentelemetry/instrumentation": "~0.52.1", From 8ed947ca654482ddeb3b611c50bb43e138f9a5b6 Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Fri, 27 Sep 2024 17:12:39 -0400 Subject: [PATCH 4/8] Add docs, tests. --- README.md | 1 + .../src/web-vitals-autoinstrumentation.ts | 34 +++++---- .../test/web-vitals-instrumentation.test.ts | 74 ++++++++++++++++++- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index db0851f5..5da1377d 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Pass these options to the HoneycombWebSDK: | enabled | optional | boolean | `true` | Where or not to enable this auto instrumentation. | | lcp| optional| VitalOpts | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). | lcp.applyCustomAttributes| optional| function | `undefined` | A function for adding custom attributes to core web vitals spans. +| lcp.dataAttributes| optional| `string[]` | `undefined` | an array of data-* attribute names to filter. By default it will send all `data-*` attribute-value pairs with the key `lcp.element.data-someAttr`, `[]` will send none. | cls| optional| VitalOpts | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). | cls.applyCustomAttributes| optional| function | `undefined` | A function for adding custom attributes to core web vitals spans. | inp| optional| VitalOptsWithTimings | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). diff --git a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts index 28ab6504..be46eb19 100644 --- a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts +++ b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts @@ -90,7 +90,8 @@ interface VitalOpts extends ReportOpts { interface LcpVitalOpts extends VitalOpts { /** - * Will send the values of these data attributes if they appear on an LCP event + * Will filter the values of these data attributes if provided, otherwise will send all data-* attributes an LCP entry + * An empty allow list, such as { dataAttributes: [] } will disable sending data-* attributes */ dataAttributes?: string[]; } @@ -452,10 +453,7 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { span.end(); }; - onReportLCP = ( - lcp: LCPMetricWithAttribution, - lcpOpts: LcpVitalOpts = { dataAttributes: [] }, - ) => { + onReportLCP = (lcp: LCPMetricWithAttribution, lcpOpts: LcpVitalOpts = {}) => { const { applyCustomAttributes, dataAttributes } = lcpOpts; if (!this.isEnabled()) return; @@ -484,14 +482,24 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { [`${attrPrefix}.resource_load_time`]: resourceLoadDuration, }); - if (dataAttributes && dataAttributes.length >= 0) { - dataAttributes?.forEach((dataAttr) => { - const value = lcpEntry?.element?.getAttribute(dataAttr); - if (value !== null && value !== undefined) { - span.setAttribute(`${attrPrefix}.element.${dataAttr}`, value); - } - }); - } + lcpEntry?.element?.getAttributeNames().forEach((attrName) => { + // Skip non data-* attributes + if (!attrName.startsWith('data-')) { + return; + } + // If dataAttributes is supplied, skip ones not in the allow list + if ( + dataAttributes && + dataAttributes.length >= 0 && + !dataAttributes.includes(attrName) + ) { + return; + } + const attrValue = lcpEntry?.element?.getAttribute(attrName); + if (attrValue !== null && attrValue !== undefined) { + span.setAttribute(`${attrPrefix}.element.${attrName}`, attrValue); + } + }); if (applyCustomAttributes) { applyCustomAttributes(lcp, span); diff --git a/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts b/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts index 27b9f9d1..c51d6cc1 100644 --- a/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts +++ b/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts @@ -56,7 +56,9 @@ const CLSAttr = { 'cls.entries': '', 'cls.my_custom_attr': 'custom_attr', }; - +const lcpElement = document.createElement('button'); +lcpElement.setAttribute('data-foo', '42'); +lcpElement.setAttribute('data-bar', 'cats'); const LCP: LCPMetricWithAttribution = { name: 'LCP', value: 2500, @@ -72,6 +74,19 @@ const LCP: LCPMetricWithAttribution = { resourceLoadDuration: 20, elementRenderDelay: 20, resourceLoadDelay: 100, + lcpEntry: { + duration: 0, + element: lcpElement, + entryType: 'largest-contentful-paint', + id: '', + loadTime: 0, + name: '', + renderTime: 74.09999999403954, + size: 4382, + startTime: 74.09999999403954, + url: '', + toJSON: () => '', + }, }, }; @@ -371,6 +386,63 @@ describe('Web Vitals Instrumentation Tests', () => { expect(exporter.getFinishedSpans().length).toEqual(1); expect(exporter.getFinishedSpans()[0].name).toEqual('LCP'); }); + + it('should include add data-* attributes when dataAttributes is undefined', () => { + const instr = new WebVitalsInstrumentation({ + vitalsToTrack: ['LCP'], + }); + instr.enable(); + instr.onReportLCP(LCP, { + applyCustomAttributes: () => {}, + dataAttributes: undefined, + }); + expect(exporter.getFinishedSpans().length).toEqual(1); + const span = exporter.getFinishedSpans()[0]; + expect(span.attributes).toMatchObject({ + 'lcp.element.data-foo': '42', + 'lcp.element.data-bar': 'cats', + }); + }); + it('should not include any data-* attributes when dataAttributes is []', () => { + const instr = new WebVitalsInstrumentation({ + vitalsToTrack: ['LCP'], + }); + instr.enable(); + instr.onReportLCP(LCP, { + applyCustomAttributes: () => {}, + dataAttributes: [], + }); + expect(exporter.getFinishedSpans().length).toEqual(1); + const span = exporter.getFinishedSpans()[0]; + const dataKeys = Object.keys(span.attributes).filter((key) => + key.startsWith('lcp.element.data-'), + ); + expect(dataKeys).toEqual([]); + expect(span.attributes['lcp.element.data-foo']).toBeUndefined(); + expect(span.attributes['lcp.element.data-bar']).toBeUndefined(); + }); + it('should only include any data-* attributes that match dataAttributes array', () => { + const instr = new WebVitalsInstrumentation({ + vitalsToTrack: ['LCP'], + }); + instr.enable(); + instr.onReportLCP(LCP, { + applyCustomAttributes: () => {}, + dataAttributes: ['data-foo'], + }); + expect(exporter.getFinishedSpans().length).toEqual(1); + const span = exporter.getFinishedSpans()[0]; + const dataKeys = Object.keys(span.attributes).filter((key) => + key.startsWith('lcp.element.data-'), + ); + expect(dataKeys).toEqual(['lcp.element.data-foo']); + expect(span.attributes['lcp.element.data-foo']).toEqual('42'); + expect(span.attributes['lcp.element.data-bar']).toBeUndefined(); + expect(span.attributes).not.toMatchObject({ + 'lcp.element.data-foo': '42', + 'lcp.element.data-bar': 'cats', + }); + }); }); describe('INP', () => { From b1c7867cfb00e4beb23ed3fc803170b2976c0a5c Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Mon, 30 Sep 2024 13:40:49 -0700 Subject: [PATCH 5/8] noop From c3f64222a27ce9bc4440f455a690695650371307 Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Tue, 8 Oct 2024 17:20:00 -0400 Subject: [PATCH 6/8] Use dataset API. --- .../examples/hello-world-web/index.html | 2 +- .../examples/hello-world-web/index.js | 2 +- .../src/web-vitals-autoinstrumentation.ts | 45 ++++++++++++------- .../test/web-vitals-instrumentation.test.ts | 43 +++++++++--------- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html index 31fd7ec2..bb25f941 100644 --- a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html +++ b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.html @@ -9,7 +9,7 @@
      -

      👋 Hello World

      +

      👋 Hello World

      diff --git a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js index 7d994b2b..fb405c0f 100644 --- a/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js +++ b/packages/honeycomb-opentelemetry-web/examples/hello-world-web/index.js @@ -27,7 +27,7 @@ const main = () => { webVitalsInstrumentationConfig: { vitalsToTrack: ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'], lcp: { - dataAttributes: ['data-hello'], + dataAttributes: ['hello', 'barBiz'], }, }, }); diff --git a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts index be46eb19..c2bec534 100644 --- a/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts +++ b/packages/honeycomb-opentelemetry-web/src/web-vitals-autoinstrumentation.ts @@ -482,24 +482,35 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract { [`${attrPrefix}.resource_load_time`]: resourceLoadDuration, }); - lcpEntry?.element?.getAttributeNames().forEach((attrName) => { - // Skip non data-* attributes - if (!attrName.startsWith('data-')) { - return; - } - // If dataAttributes is supplied, skip ones not in the allow list - if ( - dataAttributes && - dataAttributes.length >= 0 && - !dataAttributes.includes(attrName) - ) { - return; - } - const attrValue = lcpEntry?.element?.getAttribute(attrName); - if (attrValue !== null && attrValue !== undefined) { - span.setAttribute(`${attrPrefix}.element.${attrName}`, attrValue); + const el: HTMLElement = lcpEntry?.element as HTMLElement; + if (el.dataset) { + for (const attrName in el.dataset) { + const attrValue = el.dataset[attrName]; + if ( + // Value exists (including the empty string AND either + attrValue !== undefined && + // dataAttributes is undefined (i.e. send all values as span attributes) OR + (dataAttributes === undefined || + // dataAttributes is specified AND attrName is in dataAttributes (i.e attribute name is in the supplied allowList) + (dataAttributes && attrName in dataAttributes)) + ) { + span.setAttribute( + `${attrPrefix}.element.data.${attrName}`, + attrValue, + ); + } } - }); + } + if (dataAttributes) + dataAttributes?.forEach((attrName) => { + const attrValue = el.dataset[attrName]; + if (attrValue !== undefined) { + span.setAttribute( + `${attrPrefix}.element.data.${attrName}`, + attrValue, + ); + } + }); if (applyCustomAttributes) { applyCustomAttributes(lcp, span); diff --git a/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts b/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts index c51d6cc1..932c9b00 100644 --- a/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts +++ b/packages/honeycomb-opentelemetry-web/test/web-vitals-instrumentation.test.ts @@ -56,9 +56,10 @@ const CLSAttr = { 'cls.entries': '', 'cls.my_custom_attr': 'custom_attr', }; -const lcpElement = document.createElement('button'); -lcpElement.setAttribute('data-foo', '42'); -lcpElement.setAttribute('data-bar', 'cats'); +const div = document.createElement('div'); +div.innerHTML = `
      👋 Hello World
      `; +const lcpElement = div.firstElementChild; + const LCP: LCPMetricWithAttribution = { name: 'LCP', value: 2500, @@ -387,7 +388,7 @@ describe('Web Vitals Instrumentation Tests', () => { expect(exporter.getFinishedSpans()[0].name).toEqual('LCP'); }); - it('should include add data-* attributes when dataAttributes is undefined', () => { + it('should include add data-* attributes as span attributes when dataAttributes is undefined', () => { const instr = new WebVitalsInstrumentation({ vitalsToTrack: ['LCP'], }); @@ -399,8 +400,9 @@ describe('Web Vitals Instrumentation Tests', () => { expect(exporter.getFinishedSpans().length).toEqual(1); const span = exporter.getFinishedSpans()[0]; expect(span.attributes).toMatchObject({ - 'lcp.element.data-foo': '42', - 'lcp.element.data-bar': 'cats', + 'lcp.element.data.answer': '42', + 'lcp.element.data.famousCats': 'Mr. Mistoffelees', + 'lcp.element.data.hasCats': '', }); }); it('should not include any data-* attributes when dataAttributes is []', () => { @@ -414,12 +416,11 @@ describe('Web Vitals Instrumentation Tests', () => { }); expect(exporter.getFinishedSpans().length).toEqual(1); const span = exporter.getFinishedSpans()[0]; - const dataKeys = Object.keys(span.attributes).filter((key) => - key.startsWith('lcp.element.data-'), - ); - expect(dataKeys).toEqual([]); - expect(span.attributes['lcp.element.data-foo']).toBeUndefined(); - expect(span.attributes['lcp.element.data-bar']).toBeUndefined(); + expect(span.attributes).not.toMatchObject({ + 'lcp.element.data.answer': '42', + 'lcp.element.data.famousCats': 'Mr. Mistoffelees', + 'lcp.element.data.hasCats': '', + }); }); it('should only include any data-* attributes that match dataAttributes array', () => { const instr = new WebVitalsInstrumentation({ @@ -428,19 +429,19 @@ describe('Web Vitals Instrumentation Tests', () => { instr.enable(); instr.onReportLCP(LCP, { applyCustomAttributes: () => {}, - dataAttributes: ['data-foo'], + dataAttributes: ['answer'], }); expect(exporter.getFinishedSpans().length).toEqual(1); const span = exporter.getFinishedSpans()[0]; - const dataKeys = Object.keys(span.attributes).filter((key) => - key.startsWith('lcp.element.data-'), - ); - expect(dataKeys).toEqual(['lcp.element.data-foo']); - expect(span.attributes['lcp.element.data-foo']).toEqual('42'); - expect(span.attributes['lcp.element.data-bar']).toBeUndefined(); + expect(span.attributes['lcp.element.data.answer']).toEqual('42'); + expect(span.attributes['lcp.element.famousCats']).toBeUndefined(); + expect(span.attributes['lcp.element.hasCats']).toBeUndefined(); + expect(span.attributes).toMatchObject({ + 'lcp.element.data.answer': '42', + }); expect(span.attributes).not.toMatchObject({ - 'lcp.element.data-foo': '42', - 'lcp.element.data-bar': 'cats', + 'lcp.element.data.famousCats': 'Mr. Mistoffelees', + 'lcp.element.data.hasCats': '', }); }); }); From ae0ce755e6cac37300580552124706c2b00d4dc2 Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Wed, 9 Oct 2024 11:24:42 -0400 Subject: [PATCH 7/8] Update docs. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5da1377d..c7b457ee 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Pass these options to the HoneycombWebSDK: | enabled | optional | boolean | `true` | Where or not to enable this auto instrumentation. | | lcp| optional| VitalOpts | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). | lcp.applyCustomAttributes| optional| function | `undefined` | A function for adding custom attributes to core web vitals spans. -| lcp.dataAttributes| optional| `string[]` | `undefined` | an array of data-* attribute names to filter. By default it will send all `data-*` attribute-value pairs with the key `lcp.element.data-someAttr`, `[]` will send none. +| lcp.dataAttributes| optional| `string[]` | `undefined` | An array of attribute names to filter reported as `lcp.element.data.someAttr`
    • `undefined` will send all `data-*` attribute-value pairs.
    • `[]` will send none
    • `['myAttr']` will send `data-my-attr`

      An attribute that's defined, but that has no specified value like `

      ` will be sent as `{`lcp.element.data.myAttr`: '' }` | cls| optional| VitalOpts | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). | cls.applyCustomAttributes| optional| function | `undefined` | A function for adding custom attributes to core web vitals spans. | inp| optional| VitalOptsWithTimings | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). From 217f7084d5d57eb1aa71b284c8f8675f1caed150 Mon Sep 17 00:00:00 2001 From: Wolfgang Therrien Date: Wed, 9 Oct 2024 11:27:53 -0400 Subject: [PATCH 8/8] Update docs. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7b457ee..33b94c24 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Pass these options to the HoneycombWebSDK: | enabled | optional | boolean | `true` | Where or not to enable this auto instrumentation. | | lcp| optional| VitalOpts | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). | lcp.applyCustomAttributes| optional| function | `undefined` | A function for adding custom attributes to core web vitals spans. -| lcp.dataAttributes| optional| `string[]` | `undefined` | An array of attribute names to filter reported as `lcp.element.data.someAttr`
    • `undefined` will send all `data-*` attribute-value pairs.
    • `[]` will send none
    • `['myAttr']` will send `data-my-attr`

      An attribute that's defined, but that has no specified value like `

      ` will be sent as `{`lcp.element.data.myAttr`: '' }` +| lcp.dataAttributes| optional| `string[]` | `undefined` | An array of attribute names to filter reported as `lcp.element.data.someAttr`
    • `undefined` will send all `data-*` attribute-value pairs.
    • `[]` will send none
    • `['myAttr']` will send the value of `data-my-attr` or `''` if it's not supplied.

      Note: An attribute that's defined, but that has no specified value such as `

      ` will be sent as `{`lcp.element.data.myAttr`: '' }` which is inline with the [dataset API]( https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset). | cls| optional| VitalOpts | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts). | cls.applyCustomAttributes| optional| function | `undefined` | A function for adding custom attributes to core web vitals spans. | inp| optional| VitalOptsWithTimings | `undefined` | Pass-through config options for web-vitals. See [ReportOpts](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#reportopts).