elastic / apm-agent-rum-js

https://www.elastic.co/guide/en/apm/agent/rum-js/current/index.html
MIT License
279 stars 134 forks source link

LCP - customization #1319

Open trainings opened 1 year ago

trainings commented 1 year ago

By default (now) this rum agent extract this metric via browser api which works like this: https://web.dev/i18n/en/lcp/#what-elements-are-considered

But unfortunately it works not very well for Boring/Enterprise Applications which contain a lot of tables/forms etc, and very few images, etc. In fact, we very often encounter a situation for SPA applications where LCP calculated by user icon in right corner and this photo loaded and rendered before the table with business data (fetched via API). Roughly speaking, the default algorithm is false positive in such cases.

We would like to enable developers to mark critical (most important) elements on the pages and measure LCP by this elements instead of default algorithm, like described here: https://web.dev/i18n/en/custom-metrics/#element-timing-api

Such custom approach will allow us to measure and optimize aspects of our site's experience that have unique case by case for each applications.

I hope that this fix could be done ~here https://github.com/elastic/apm-agent-rum-js/blob/main/packages/rum-core/src/performance-monitoring/metrics.js#L245

Th target solution could work:

  1. if agent find marked elements - LCP should be calculated by this elements via timing api
  2. if no marked elements - LCP should be calculated by default (just like now)
devcorpio commented 1 year ago

Hi @trainings,

Thanks for opening such a detailed issue, much appreciated!

Since this is something that would require new development, I would like to know if this workaround can help you in the meanwhile:

<!DOCTYPE html>
<html>
    <head>
        <script src="https://unpkg.com/@elastic/apm-rum@5.12.0/dist/bundles/elastic-apm-rum.umd.min.js"></script>
    </head>
    <body>
        Sample app
        <input type="button" value="click me" id="button-clickable" elementtiming="text">
        <input type="button" elementtiming="text" value="click me 2" id="button-redirect">
        <script>
               elasticApm.init({
                    serviceName: "your-service",
                    serverUrl: "http://localhost:8200/"
               })

              // Just after the agent initialization start observing element and kept reference if needed
               let timingEntry;
               const observer = new PerformanceObserver((list) => {
                    list.getEntries().forEach((entry) => {
                        // your logic, just an example
                        timingEntry = entry
                    });
                });

                observer.observe({ entryTypes: ["element"]});
        </script>
        <script>

            // observe the page load transaction end
            // then, you can replace the calculate LCP
            // with the value extracted from element timing if needed
            elasticApm.observe("transaction:end", tr => {
                if (tr.type === 'page-load') {
                   if (tr.marks.agent.largestContentfulPaint) {
                       // your logic here
                       // you can overwrite the LCP value here if needed
                       tr.marks.agent.largestContentfulPaint = "your-calculated-value-here"
                   }
                }
            })
        </script>
        <input type="button" elementtiming="text" value="last button" id="button-redirect-last">

    </body>
</html>
  1. Start observing element timing entries just after initializing the agent.
  2. observe the page-load transaction end and replace the calculated LCP if needed
  3. you might want to stop observing element timing entries once page load event is triggered

P.S. I added a few comments in the snippet.

Thanks, Alberto

Shestak2039 commented 1 year ago

Hi @devcorpio,

I have another question. If the PerfomanceObserver is triggered after sending the page-load transaction. Maybe we can somehow postpone the sending of this transaction? Or maybe there are better solutions? Ultimately, my LCP is larger than the entire page-load.

I am using the angular version of elastic.

devcorpio commented 1 year ago

Hi @Shestak2039,

At this moment, we are doing something similar to what you mentioned. Once the browser fires the load event, the RUM agent waits 1 extra second before sending the transaction. The goal of that was to capture "late" LCPs entries. We did that on this PR.

Thanks, Alberto

Shestak2039 commented 1 year ago

@devcorpio

I and the author of the issue are from the same team. And we have a problem that one second is not enough. We have a list of elements that takes a long time to return, and we've written elementtiming inside the first element of the table to treat it as an LCP. But now the page-load transaction is sent before we get the rendering time of the first element of the table. Maybe you can suggest some solutions? Or maybe you can make this time configurable when initializing the elastic in the application?

Thanks, Vlad

devcorpio commented 1 year ago

Hi @Shestak2039,

This is a workaround using the current agent APIs that might help you.

The strategy to follow would be:

The code would be something like this:

window.addEventListener('load', () => {
    // This will retrieve the current transaction (page load)
    const tr = elasticApm.getCurrentTransaction();

    // You can attach a span to it:
    const span = tr.startSpan("my-span", "my-span-type", { blocking: true })

    // you will need to keep a reference of your span variable.
    // Once you have the data you need to report, you can call:

    span.end()

    // on this particular case, span.end() will call end the transaction automatically
})

You will see that I created a span that extends the transaction's life. (via blocking config option)

Then, since you will decide when the span ends (based on your business logic) you could then overwrite the LCP if needed as suggested in a previous comment. You would also be able to create labels dynamically if needed

Please, let me know if this helps you.

Thanks, Alberto

Shestak2039 commented 1 year ago

Hi @devcorpio, I have another question) Is it possible to somehow pass LCP element selector to apm? I'm trying to use your old solution where we replaced the LCP, but the string I am sending cannot be seen.

devcorpio commented 1 year ago

Hi @Shestak2039,

You should be able to do that via labels. You can add labels to an existing transaction. I'm assuming that you want to attach it to the page-load transaction, is that right?

Example:

            elasticApm.observe("transaction:end", tr => {
                if (tr.type === 'page-load') {
                    tr.addLabels({ lcpElementSelector: "your-selector" })
                }
            })

You should be able to see that in the transaction details:

Screenshot 2023-03-29 at 19 52 01

Also in discover:

Screenshot 2023-03-29 at 19 53 30

Let me know if this helps you. In case it is not, would you mind sharing the snippet you are using?

Cheers, Alberto

Shestak2039 commented 1 year ago

Yes, I would like to add to it, thanks. I will check and answer.

Shestak2039 commented 1 year ago

@devcorpio Thanks, it works. One question is when we use addLabel. Can't we add a number to the value?