Skip to content

Commit bda1e1a

Browse files
Merge pull request #3 from EcomDev/feature/module
feat: Magento Module for RUM Beacon
2 parents 5b6896b + c0e986f commit bda1e1a

40 files changed

+2601
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor/
2+
/node_modules/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 EcomDev B.V.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

composer.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "ecomdev/magento2-module-rum-beacon",
3+
"description": "Magento 2 RUM Beacon",
4+
"type": "magento2-module",
5+
"require": {
6+
"magento/framework": "*",
7+
"magento/module-page-cache": "*",
8+
"magento/module-catalog": "*"
9+
},
10+
"license": [
11+
"MIT"
12+
],
13+
"autoload": {
14+
"files": [
15+
"src/registration.php"
16+
],
17+
"psr-4": {
18+
"EcomDev\\RUMBeacon\\": "src/"
19+
}
20+
}
21+
}

lib/beacon.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {loadConfiguration} from "./configuration";
2+
import collectPageData from "./page-info";
3+
import {canSend, Payload, send} from "./payload";
4+
import timeToFirstByte from "./time-to-first-byte";
5+
import firstContentfulPaint from "./first-contentful-paint";
6+
import largestContentfulPaint from "./largest-contentful-paint";
7+
import comulativeLayoutShift from "./comulative-layout-shift";
8+
import interactionToNextPaint from "./interaction-to-next-paint";
9+
10+
11+
/**
12+
* Main beacon routine
13+
*
14+
* Sets up event handers and send data to external service when page visibility changes
15+
*/
16+
function initializeIngestor() {
17+
const config = loadConfiguration();
18+
19+
if (config === false) {
20+
return;
21+
}
22+
23+
const payload: Payload = {
24+
pageInfo: collectPageData(config)
25+
};
26+
27+
timeToFirstByte(payload);
28+
firstContentfulPaint(payload);
29+
largestContentfulPaint(payload);
30+
comulativeLayoutShift(payload);
31+
interactionToNextPaint(payload);
32+
33+
addEventListener('visibilitychange', () => {
34+
if (document.visibilityState === 'hidden') {
35+
if (canSend(payload)) {
36+
send(config, payload)
37+
}
38+
}
39+
});
40+
}
41+
42+
// Make sure we don't block main thread on load
43+
setTimeout(initializeIngestor);

lib/common.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface SimpleDictionary {
2+
[Key: string]: string | number | boolean;
3+
}

lib/comulative-layout-shift.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Payload} from "./payload";
2+
import {onCLS} from "web-vitals/attribution.js";
3+
4+
export default function (payload: Payload) {
5+
onCLS(metric => {
6+
payload.cls = {
7+
value: metric.value,
8+
navigationType: metric.navigationType,
9+
target: metric.attribution.largestShiftTarget ?? '',
10+
time: metric.attribution.largestShiftTime ?? 0
11+
}
12+
})
13+
}

lib/configuration.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { SimpleDictionary } from "./common";
2+
3+
interface AllowedQueryParameters {
4+
[Key: string]: boolean,
5+
}
6+
7+
export interface BeaconConfig {
8+
allowedQueryParams: AllowedQueryParameters,
9+
isEnabled: boolean,
10+
isUrlCollected: boolean,
11+
ingestionUrl: string,
12+
pageInfo: SimpleDictionary
13+
}
14+
15+
export function loadConfiguration(): BeaconConfig | false {
16+
const beaconIngestElement = document.querySelector('script[type="ecomdev/rum-beacon-ingest"]');
17+
if (!beaconIngestElement) {
18+
return false;
19+
}
20+
21+
const beaconConfig: BeaconConfig = JSON.parse(beaconIngestElement.textContent);
22+
23+
if (!beaconConfig.isEnabled || !beaconConfig.ingestionUrl || !beaconConfig.pageInfo) {
24+
return false;
25+
}
26+
27+
return beaconConfig;
28+
}

lib/first-contentful-paint.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Payload} from "./payload";
2+
import {onFCP} from "web-vitals/attribution.js";
3+
4+
export default function(payload: Payload) {
5+
onFCP(metric => {
6+
payload.fcp = {
7+
value: metric.value,
8+
navigationType: metric.navigationType,
9+
renderDelay: metric.attribution.firstByteToFCP,
10+
ttfb: metric.attribution.timeToFirstByte
11+
}
12+
})
13+
}

lib/interaction-to-next-paint.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {InteractionToNextPaint, Payload} from "./payload";
2+
import {onINP} from "web-vitals/attribution.js";
3+
4+
export default function(payload: Payload) {
5+
onINP(metric => {
6+
const script = metric.attribution.longestScript;
7+
8+
const inp: InteractionToNextPaint = {
9+
value: metric.value,
10+
navigationType: metric.navigationType,
11+
intersection: script?.intersectingDuration ?? 0,
12+
target: metric.attribution.interactionTarget
13+
}
14+
15+
if (script) {
16+
try {
17+
const url = new URL(script.entry.sourceURL);
18+
inp.script_domain = url.host;
19+
inp.script_path = url.pathname;
20+
} catch (e) {
21+
}
22+
23+
inp.affected_part = script.subpart;
24+
}
25+
26+
payload.inp = inp;
27+
})
28+
}

lib/largest-contentful-paint.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Payload} from "./payload";
2+
import {onLCP} from "web-vitals/attribution.js";
3+
4+
export default function(payload: Payload) {
5+
onLCP(metric => {
6+
payload.lcp = {
7+
value: metric.value,
8+
navigationType: metric.navigationType,
9+
load: metric.attribution.resourceLoadDuration,
10+
loadDelay: metric.attribution.resourceLoadDelay,
11+
renderDelay: metric.attribution.elementRenderDelay,
12+
ttfb: metric.attribution.timeToFirstByte,
13+
target: metric.attribution.target ?? '',
14+
url: metric.attribution.url ?? ''
15+
}
16+
})
17+
}

0 commit comments

Comments
 (0)