Skip to content

Commit

Permalink
feat(honeycomb-opentelemetry-web): Add data attributtes for LCP. (#309)
Browse files Browse the repository at this point in the history
<!--
Thank you for contributing to the project! 💜
Please see our [OSS process
document](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md#)
to get an idea of how we operate.
-->

## Which problem is this PR solving?
Adds `data-` attributes to LCP web vitals and a configuration to specify
which data attributes to collect.

Closes #95 

## Short description of the changes

## How to verify that this has the expected result

Add a `data-` to the LCP element and see it's value as an attribute span
under `element.data-ATTRIBUTE_NAME`

![CleanShot 2024-09-26 at 16 38
09@2x](https://github.com/user-attachments/assets/43898db1-c113-469e-816e-96cc1b215a27)
  • Loading branch information
wolfgangcodes authored Oct 9, 2024
1 parent 20e7aa9 commit c77fb9d
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 103 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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` <br/> <li/> `undefined` will send all `data-*` attribute-value pairs. <li/> `[]` will send none <li/> `['myAttr']` will send the value of `data-my-attr` or `''` if it's not supplied. <p/> Note: An attribute that's defined, but that has no specified value such as `<div data-my-attr />` 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).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Honeycomb OpenTelemetry Web Distro</title>
</head>
<body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Honeycomb OpenTelemetry Web Distro</title>
</head>
<body>
<ul id="spans-go-here"></ul>
<section class="example-app">
<header class="header">
<h1>👋 Hello World</h1>
</header>
<section class="example-app">
<header class="header">
<h1 data-hello="hello" data-foo="42" data-bar-biz>👋 Hello World</h1>
</header>

<button id="loadDadJoke">Get A Random Dad Joke</button>
<div>
<span id="dadJokeText"></span>
</div>
</section>
<!-- Scripts here. Don't remove ↓ -->
<script type="module" src="build/bundle.js"></script>
</body>
<button id="loadDadJoke">Get A Random Dad Joke</button>
<div>
<span id="dadJokeText"></span>
</div>
</section>
<!-- Scripts here. Don't remove ↓ -->
<script type="module" src="build/bundle.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const main = () => {
contextManager: new ZoneContextManager(),
webVitalsInstrumentationConfig: {
vitalsToTrack: ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'],
lcp: {
dataAttributes: ['hello', 'barBiz'],
},
},
});
sdk.start();
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,19 @@ interface VitalOpts extends ReportOpts {
applyCustomAttributes?: ApplyCustomAttributesFn;
}

interface VitalOptsWithTimings extends VitalOpts {
interface LcpVitalOpts extends VitalOpts {
/**
* 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[];
}

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:
Expand Down Expand Up @@ -210,13 +218,13 @@ export interface WebVitalsInstrumentationConfig extends InstrumentationConfig {
vitalsToTrack?: Array<Metric['name']>;

/** 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;
Expand All @@ -235,9 +243,9 @@ export interface WebVitalsInstrumentationConfig extends InstrumentationConfig {
*/
export class WebVitalsInstrumentation extends InstrumentationAbstract {
readonly vitalsToTrack: Array<Metric['name']>;
readonly lcpOpts?: VitalOpts;
readonly lcpOpts?: LcpVitalOpts;
readonly clsOpts?: VitalOpts;
readonly inpOpts?: VitalOptsWithTimings;
readonly inpOpts?: InpVitalOpts;
readonly fidOpts?: VitalOpts;
readonly fcpOpts?: VitalOpts;
readonly ttfbOpts?: VitalOpts;
Expand Down Expand Up @@ -280,41 +288,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);
}
}
Expand Down Expand Up @@ -417,10 +421,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;
Expand Down Expand Up @@ -451,10 +453,8 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract {
span.end();
};

onReportLCP = (
lcp: LCPMetricWithAttribution,
applyCustomAttributes?: ApplyCustomAttributesFn,
) => {
onReportLCP = (lcp: LCPMetricWithAttribution, lcpOpts: LcpVitalOpts = {}) => {
const { applyCustomAttributes, dataAttributes } = lcpOpts;
if (!this.isEnabled()) return;

const { name, attribution } = lcp;
Expand All @@ -465,6 +465,7 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract {
resourceLoadDelay,
resourceLoadDuration,
elementRenderDelay,
lcpEntry,
}: LCPAttribution = attribution;
const attrPrefix = this.getAttrPrefix(name);

Expand All @@ -481,6 +482,36 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract {
[`${attrPrefix}.resource_load_time`]: resourceLoadDuration,
});

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);
}
Expand All @@ -490,9 +521,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;
Expand Down Expand Up @@ -550,10 +581,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;
Expand All @@ -580,10 +609,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;
Expand All @@ -608,8 +635,9 @@ export class WebVitalsInstrumentation extends InstrumentationAbstract {

onReportTTFB = (
ttfb: TTFBMetricWithAttribution,
applyCustomAttributes?: ApplyCustomAttributesFn,
ttfbOpts: VitalOpts = {},
) => {
const { applyCustomAttributes } = ttfbOpts;
if (!this.isEnabled()) return;

const { name, attribution } = ttfb;
Expand Down
Loading

0 comments on commit c77fb9d

Please sign in to comment.