Skip to content

feat: MFE core web-vitals detection#1768

Merged
metal-messiah merged 26 commits into
mainfrom
mfe-fcp-poc
Jun 23, 2026
Merged

feat: MFE core web-vitals detection#1768
metal-messiah merged 26 commits into
mainfrom
mfe-fcp-poc

Conversation

@metal-messiah

@metal-messiah metal-messiah commented May 5, 2026

Copy link
Copy Markdown
Member

Add internal detection methods for FCP, INP, CLS, and LCP when spawned by a MFE

Overview

This PR implements comprehensive Web Vitals tracking for Micro Frontend (MFE) components, adding detection and metadata collection for four Core Web Vitals metrics: First Contentful Paint (FCP), Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP).

Key Changes

Unified Vitals Object Structure
Each vital metric is tracked as a unified object containing both the measurement value and rich contextual metadata. These objects are reported directly as JSON in the MicroFrontEndTiming event:

{
  'nr.vitals.fcp': {
    value: 1234.56,      // Timestamp when FCP occurred
    loadState: 'loading'  // Document readyState at FCP
  },
  'nr.vitals.lcp': {
    value: 2345.67,      // Timestamp when LCP occurred
    size: 480000,        // Element size in pixels (width × height)
    elTag: 'IMG',        // HTML tag name of LCP element
    eid: 'hero-image',   // Element ID (if present)
    elUrl: 'https://...' // Element URL (images/videos/background-images)
  },
  'nr.vitals.cls': {
    value: 0.123,                    // Cumulative layout shift score
    largestShiftValue: 0.05,         // Value of largest individual shift
    largestShiftTime: 1234,          // Timestamp of largest shift
    largestShiftTarget: 'div#hero',  // CSS selector of element that shifted
    loadState: 'complete'            // Document readyState when measured
  },
  'nr.vitals.inp': {
    value: 250,                      // Total interaction latency (ms)
    interactionTarget: 'button#submit', // CSS selector of interacted element
    interactionTime: 1234,           // Timestamp of interaction
    interactionType: 'click',        // Interaction type (click, keydown, etc.)
    inputDelay: 10,                  // Input delay (ms)
    processingDuration: 190,         // Event processing time (ms)
    presentationDelay: 50,           // Presentation delay (ms)
    nextPaintTime: 1484,             // Next paint timestamp
    loadState: 'interactive'         // Document readyState during interaction
  }
}

MicroFrontEndTiming Event Attributes
When an MFE deregisters (via deregister() API), a MicroFrontEndTiming event is generated with the following JSON object attributes:

Important: these attributes are prefixed with nr. to ensure they are hidden while being evaluated

nr.vitals.fcp (object or null):

  • value - Time from script start to FCP (milliseconds)
  • loadState - Document state when FCP occurred ('loading', 'interactive', or 'complete')

nr.vitals.lcp (object or null):

  • value - Time from script start to LCP (milliseconds)
  • elTag - HTML tag of the LCP element
  • eid - Element ID attribute (if present)
  • elUrl - URL for image/video elements or background images
  • size - Element dimensions in pixels (width × height)

nr.vitals.cls (object or null):

  • value - Cumulative layout shift score (0-1+)
  • largestShiftTarget - CSS selector of element causing largest shift
  • largestShiftTime - Timestamp of largest individual shift
  • largestShiftValue - Value of largest individual shift
  • loadState - Document state when CLS was measured

nr.vitals.inp (object or null):

  • value - Worst interaction latency in milliseconds
  • interactionTarget - CSS selector of interacted element
  • interactionTime - Timestamp when interaction occurred
  • interactionType - Type of interaction (click, keydown, keypress, etc.)
  • inputDelay - Delay before event processing began (ms)
  • processingDuration - Duration of event handler execution (ms)
  • presentationDelay - Delay from processing to paint (ms)
  • nextPaintTime - Timestamp when paint occurred
  • loadState - Document state during interaction
sequenceDiagram
    participant App as MFE Application
    participant API as Register API
    participant Tracker as trackMFEVitals
    participant MO as MutationObserver
    participant PO as PerformanceObserver
    participant Events as Event Harvester

    App->>API: newrelic.register({ id, name })
    API->>Tracker: trackMFEVitals(mfeId)
    
    Note over Tracker: Initialize vitals object<br/>{ fcp, lcp, cls, inp } = null
    
    Tracker->>MO: Create FCP Observer
    Note over MO: Watches for first<br/>contentful paint
    Tracker->>MO: Create LCP Observer
    Note over MO: Watches for largest<br/>contentful paint
    Tracker->>PO: Create CLS Observer
    Note over PO: Watches for layout<br/>shift entries
    Tracker->>PO: Create INP Observer
    Note over PO: Watches for interaction<br/>event entries

    par Vitals Detection
        MO-->>Tracker: FCP detected
        Note over Tracker: vitals.fcp = {<br/>value, loadState<br/>}
        MO-->>Tracker: LCP detected (updates on larger elements)
        Note over Tracker: vitals.lcp = {<br/>value, size, elTag,<br/>eid, elUrl<br/>}
        PO-->>Tracker: CLS shift detected
        Note over Tracker: vitals.cls = {<br/>value, largestShiftValue,<br/>largestShiftTime,<br/>largestShiftTarget, loadState<br/>}
        PO-->>Tracker: INP interaction detected
        Note over Tracker: vitals.inp = {<br/>value, interactionTarget,<br/>interactionTime, interactionType,<br/>inputDelay, processingDuration,<br/>presentationDelay,<br/>nextPaintTime, loadState<br/>}
    end

    App->>API: mfe.deregister()
    API->>Tracker: Get vitals
    
    Tracker->>API: Return vitals object
    API->>Events: Send MicroFrontEndTiming event
    Note over Events: Vitals sent as JSON strings:<br/>"nr.vitals.fcp": "{...}"<br/>"nr.vitals.lcp": "{...}"<br/>"nr.vitals.cls": "{...}"<br/>"nr.vitals.inp": "{...}"
Loading

Testing

Unit Tests (tests/unit/common/v2/mfe-vitals.test.js)

  • ✅ FCP tracking with loadState metadata
  • ✅ LCP tracking with element metadata (tag, id, url, size)
  • ✅ LCP updates when larger elements appear
  • ✅ CLS tracking with largest shift metadata
  • ✅ INP tracking with full interaction timeline
  • ✅ Element scope validation (only tracks within MFE boundary)
  • ✅ Observer disconnect functionality
  • ✅ Edge cases (missing IDs, SVG className objects, invalid elements)

E2E Tests (tests/specs/api/register/mfe-vitals.e2e.js)

  • ✅ Real browser FCP/LCP capture with JSON object validation
  • ✅ Metadata presence validation for all vitals
  • ✅ Value sanity checks (reasonable ranges, relative ordering)
  • ✅ Browser compatibility filters (Chromium-only tests for CLS/INP/LCP)
  • ✅ Multiple MFE isolation testing
  • ✅ Observer lifecycle and disconnection validation

Browser Compatibility:

  • FCP: All browsers supporting First Contentful Paint
  • LCP: Chromium-based browsers (Chrome, Edge, Opera)
  • CLS: Chromium-based browsers (layout-shift PerformanceObserver)
  • INP: Chromium-based browsers with PerformanceEventTiming API

Manual Testing Checklist:

  • Verify MicroFrontEndTiming events are harvested with nr.vitals.* JSON objects
  • Confirm objects are null when vitals are unsupported/not captured
  • Test multiple MFEs on same page don't cross-contaminate vitals
  • Verify JSON object structure is preserved in event payloads
  • Check that deregister() properly disconnects observers and captures final values

@metal-messiah metal-messiah added the POC A proof of concept label May 5, 2026
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Supportability Metric Usage Changes:

No matching changes found

Supportability Metrics .md File Changes:

supportability_metrics.md was changed? false

New supportability metrics require changes to supportability_metrics.md, as well as a new PR to Angler. Please ensure an Angler PR is created and linked to this PR.

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Asset Size Report

Merging this pull request will result in the following asset size changes:

Agent Asset Previous Size New Size Diff
lite loader 27.29 kB / 10.29 kB (gzip) 26.86 kB / 10.26 kB (gzip) -1.58% / -0.35% (gzip)
lite async-chunk 59.83 kB / 20.09 kB (gzip) 59.62 kB / 20.05 kB (gzip) -0.36% / -0.18% (gzip)
pro loader 67.41 kB / 23.61 kB (gzip) 70.33 kB / 24.7 kB (gzip) 4.33% / 4.63% (gzip)
pro async-chunk 97.39 kB / 30.85 kB (gzip) 97.17 kB / 30.83 kB (gzip) -0.22% / -0.09% (gzip)
spa loader 69.81 kB / 24.38 kB (gzip) 72.73 kB / 25.46 kB (gzip) 4.19% / 4.42% (gzip)
spa async-chunk 109.54 kB / 34.05 kB (gzip) 109.32 kB / 34.02 kB (gzip) -0.2% / -0.09% (gzip)

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.56198% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.12%. Comparing base (d75f4bf) to head (c12b892).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/common/v2/mfe-vitals.js 91.74% 8 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1768      +/-   ##
==========================================
+ Coverage   90.09%   90.12%   +0.03%     
==========================================
  Files         214      215       +1     
  Lines        7852     7972     +120     
  Branches     1669     1736      +67     
==========================================
+ Hits         7074     7185     +111     
- Misses        708      716       +8     
- Partials       70       71       +1     
Flag Coverage Δ
unit-tests 86.19% <92.56%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@metal-messiah metal-messiah changed the title chore: Mfe fcp poc chore: Mfe vitals poc May 18, 2026
@metal-messiah metal-messiah marked this pull request as ready for review May 20, 2026 20:42
@metal-messiah metal-messiah changed the title chore: Mfe vitals poc feat: MFE core web-vitals detection May 20, 2026
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Static Badge

Last ran on June 10, 2026 16:22:01 CDT
Checking merge of (3aca09c) into main (65b2e3b)

@metal-messiah metal-messiah removed the POC A proof of concept label Jun 3, 2026
Comment thread src/common/v2/mfe-vitals.js
Comment thread src/common/v2/mfe-vitals.js
let clsValue = 0
const clsObs = observePerformance(observers, { type: 'layout-shift', buffered: true }, (entry) => {
if (entry.hadRecentInput || !vitals.cls) return
(entry.sources || []).some(source => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion: Maybe consider using find() to achieve similar result/return only the first matching result in a given MFE. (This started as a "style" suggestion, but thought may be worth mentioning, as the code may be slightly shorter.)

@ptang-nr

Copy link
Copy Markdown
Contributor

I also saw LCP and INP are newly supported in Firefox and Safari. Might be good to mention in the PR notes.

@metal-messiah

Copy link
Copy Markdown
Member Author

will have another followup PR to refine the output after evaluation and API review, to expose valuable attributes and remove unneeded ones

@metal-messiah metal-messiah merged commit 8559842 into main Jun 23, 2026
37 checks passed
@metal-messiah metal-messiah deleted the mfe-fcp-poc branch June 23, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants