@adobe/helix-rum-enhancer 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [2.14.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.13.0...v2.14.0) (2024-06-11)
2
+
3
+
4
+ ### Features
5
+
6
+ * **viewmedia:** expand view media tracking to all images that are there at render time ([d7a1715](https://github.com/adobe/helix-rum-enhancer/commit/d7a17158cf6ef2023ca36811ac5e436b6f18847e))
7
+
8
+ # [2.13.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.12.0...v2.13.0) (2024-06-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * proper linted output ([f2548ee](https://github.com/adobe/helix-rum-enhancer/commit/f2548eeda287e5a8abf75a9dc2156643640406eb))
14
+
15
+
16
+ ### Features
17
+
18
+ * **bundle:** first step to build enhancer as a bundle ([56255db](https://github.com/adobe/helix-rum-enhancer/commit/56255db63e4d9fcd5f9dd685ee942ebda3052130))
19
+
1
20
  # [2.12.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.11.4...v2.12.0) (2024-06-10)
2
21
 
3
22
 
@@ -0,0 +1,416 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-disable max-classes-per-file, wrap-iife */
14
+ // eslint-disable-next-line func-names
15
+ (function () {
16
+ 'use strict';
17
+
18
+ /* eslint-env browser */
19
+ const KNOWN_PROPERTIES = ['weight', 'id', 'referer', 'checkpoint', 't', 'source', 'target', 'cwv', 'CLS', 'FID', 'LCP', 'INP', 'TTFB'];
20
+ const DEFAULT_TRACKING_EVENTS = ['click', 'cwv', 'form', 'enterleave', 'viewblock', 'viewmedia', 'loadresource', 'utm'];
21
+ const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum : {};
22
+
23
+ const fflags = {
24
+ has: (flag) => fflags[flag].indexOf(Array.from(window.origin)
25
+ .map((a) => a.charCodeAt(0))
26
+ .reduce((a, b) => a + b, 1) % 1371) !== -1,
27
+ enabled: (flag, callback) => fflags.has(flag) && callback(),
28
+ disabled: (flag, callback) => !fflags.has(flag) && callback(),
29
+ onetrust: [543, 770, 1136],
30
+ ads: [1139, 543, 770, 984],
31
+ email: [1139, 543, 770, 984],
32
+ };
33
+
34
+ const urlSanitizers = {
35
+ full: () => window.location.href,
36
+ origin: () => window.location.origin,
37
+ path: () => window.location.href.replace(/\?.*$/, ''),
38
+ };
39
+
40
+ const getTargetValue = (element) => element.getAttribute('data-rum-target') || element.getAttribute('href')
41
+ || element.currentSrc || element.getAttribute('src') || element.dataset.action || element.action;
42
+
43
+ const targetselector = (element) => {
44
+ try {
45
+ if (!element) return undefined;
46
+ let value = getTargetValue(element);
47
+ if (!value && element.tagName !== 'A' && element.closest('a')) {
48
+ value = getTargetValue(element.closest('a'));
49
+ }
50
+ if (value && !value.startsWith('https://')) {
51
+ // resolve relative links
52
+ value = new URL(value, window.location).href;
53
+ }
54
+ return value;
55
+ } catch (error) {
56
+ // something went wrong
57
+ return null;
58
+ }
59
+ };
60
+
61
+ const sourceselector = (element) => {
62
+ try {
63
+ if (element === document.body || element === document.documentElement || !element) {
64
+ return undefined;
65
+ }
66
+ if (element.getAttribute('data-rum-source')) {
67
+ return element.getAttribute('data-rum-source');
68
+ }
69
+ const form = element.closest('form');
70
+ let formElementSelector = '';
71
+ if (form && Array.from(form.elements).includes(element)) {
72
+ formElementSelector = element.tagName === 'INPUT' ? `form input[type='${element.getAttribute('type')}']` : `form ${element.tagName.toLowerCase()}`;
73
+ }
74
+
75
+ const blockName = element.closest('.block') ? element.closest('.block').getAttribute('data-block-name') : '';
76
+ if (element.id || formElementSelector) {
77
+ const id = element.id ? `#${element.id}` : '';
78
+ return blockName ? `.${blockName} ${formElementSelector}${id}` : `${formElementSelector}${id}`;
79
+ }
80
+
81
+ if (element.getAttribute('data-block-name')) {
82
+ return `.${element.getAttribute('data-block-name')}`;
83
+ }
84
+
85
+ if (Array.from(element.classList).some((className) => className.match(/button|cta/))) {
86
+ return blockName ? `.${blockName} .button` : '.button';
87
+ }
88
+
89
+ return sourceselector(element.parentElement);
90
+ } catch (error) {
91
+ return null;
92
+ }
93
+ };
94
+
95
+ const formSubmitListener = (e) => sampleRUM('formsubmit', { target: targetselector(e.target), source: sourceselector(e.target) });
96
+ // eslint-disable-next-line no-use-before-define
97
+ const mutationObserver = window.MutationObserver ? new MutationObserver(mutationsCallback) : null;
98
+ // Gets configured collection from the config service for the current domain
99
+ function getCollectionConfig() {
100
+ // eslint-disable-next-line max-len
101
+ fflags.enabled('onetrust', () => DEFAULT_TRACKING_EVENTS.push('consent'));
102
+ fflags.enabled('ads', () => DEFAULT_TRACKING_EVENTS.push('paid'));
103
+ fflags.enabled('email', () => DEFAULT_TRACKING_EVENTS.push('email'));
104
+ return DEFAULT_TRACKING_EVENTS;
105
+ }
106
+
107
+ function trackCheckpoint(checkpoint, data, t) {
108
+ const { weight, id } = window.hlx.rum;
109
+ if (isSelected) {
110
+ const sendPing = (pdata = data) => {
111
+ // eslint-disable-next-line object-curly-newline, max-len
112
+ const body = JSON.stringify({ weight, id, referer: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'](), checkpoint, t, ...data }, KNOWN_PROPERTIES);
113
+ const { href: url, origin } = new URL(`.rum/${weight}`, sampleRUM.collectBaseURL || sampleRUM.baseURL);
114
+ if (window.location.origin === origin) {
115
+ const headers = { type: 'application/json' };
116
+ navigator.sendBeacon(url, new Blob([body], headers));
117
+ } else {
118
+ navigator.sendBeacon(url, body);
119
+ }
120
+ // eslint-disable-next-line no-console
121
+ console.debug(`ping:${checkpoint}`, pdata);
122
+ };
123
+ sendPing(data);
124
+ }
125
+ }
126
+
127
+ function processQueue() {
128
+ while (queue.length) {
129
+ const ck = queue.shift();
130
+ trackCheckpoint(...ck);
131
+ }
132
+ }
133
+
134
+ function addCWVTracking() {
135
+ setTimeout(() => {
136
+ try {
137
+ const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
138
+ if (document.querySelector(`script[src="${cwvScript}"]`)) {
139
+ // web vitals script has been loaded already
140
+ return;
141
+ }
142
+ const script = document.createElement('script');
143
+ script.src = cwvScript;
144
+ script.onload = () => {
145
+ const storeCWV = (measurement) => {
146
+ const data = { cwv: {} };
147
+ data.cwv[measurement.name] = measurement.value;
148
+ if (measurement.name === 'LCP' && measurement.entries.length > 0) {
149
+ const { element } = measurement.entries.pop();
150
+ data.target = targetselector(element);
151
+ data.source = sourceselector(element) || (element && element.outerHTML.slice(0, 30));
152
+ }
153
+ sampleRUM('cwv', data);
154
+ };
155
+
156
+ const featureToggle = () => window.location.hostname === 'blog.adobe.com';
157
+ const isEager = (metric) => ['CLS', 'LCP'].includes(metric);
158
+
159
+ // When loading `web-vitals` using a classic script, all the public
160
+ // methods can be found on the `webVitals` global namespace.
161
+ ['FID', 'INP', 'TTFB', 'CLS', 'LCP'].forEach((metric) => {
162
+ const metricFn = window.webVitals[`on${metric}`];
163
+ if (typeof metricFn === 'function') {
164
+ const opts = isEager(metric) ? { reportAllChanges: featureToggle() } : undefined;
165
+ metricFn(storeCWV, opts);
166
+ }
167
+ });
168
+ };
169
+ document.head.appendChild(script);
170
+ } catch (error) {
171
+ // something went wrong
172
+ }
173
+ }, 2000); // wait for delayed
174
+ }
175
+
176
+ function addEnterLeaveTracking() {
177
+ // enter checkpoint when referrer is not the current page url
178
+ const navigate = (source, type) => {
179
+ const payload = { source, target: document.visibilityState };
180
+ // reload: same page, navigate: same origin, enter: everything else
181
+ if (type === 'reload' || source === window.location.href) {
182
+ sampleRUM('reload', payload);
183
+ } else if (type && type !== 'navigate') {
184
+ sampleRUM(type, payload); // back, forward, prerender, etc.
185
+ } else if (source && window.location.origin === new URL(source).origin) {
186
+ sampleRUM('navigate', payload); // internal navigation
187
+ } else {
188
+ sampleRUM('enter', payload); // enter site
189
+ }
190
+ };
191
+
192
+ new PerformanceObserver((list) => list
193
+ .getEntries().map((entry) => navigate(document.referrer, entry.type)))
194
+ .observe({ type: 'navigation', buffered: true });
195
+
196
+ const leave = ((event) => {
197
+ try {
198
+ if (leave.left || (event.type === 'visibilitychange' && document.visibilityState !== 'hidden')) {
199
+ return;
200
+ }
201
+ leave.left = true;
202
+ sampleRUM('leave');
203
+ } catch (error) {
204
+ // something went wrong
205
+ }
206
+ });
207
+ window.addEventListener('visibilitychange', ((event) => leave(event)));
208
+ window.addEventListener('pagehide', ((event) => leave(event)));
209
+ }
210
+
211
+ function addLoadResourceTracking() {
212
+ const observer = new PerformanceObserver((list) => {
213
+ try {
214
+ list.getEntries()
215
+ .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
216
+ .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
217
+ .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html|\\.json)$'))
218
+ .forEach((entry) => {
219
+ sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
220
+ });
221
+ list.getEntries()
222
+ .filter((entry) => entry.responseStatus === 404)
223
+ .forEach((entry) => {
224
+ sampleRUM('missingresource', { source: entry.name, target: entry.hostname });
225
+ });
226
+ } catch (error) {
227
+ // something went wrong
228
+ }
229
+ });
230
+ observer.observe({ type: 'resource', buffered: true });
231
+ }
232
+
233
+ function activateBlocksMutationObserver() {
234
+ if (!mutationObserver || mutationObserver.active) {
235
+ return;
236
+ }
237
+ mutationObserver.active = true;
238
+ mutationObserver.observe(
239
+ document.body,
240
+ // eslint-disable-next-line object-curly-newline
241
+ { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
242
+ );
243
+ }
244
+
245
+ function getIntersectionObsever(checkpoint) {
246
+ if (!window.IntersectionObserver) {
247
+ return null;
248
+ }
249
+ activateBlocksMutationObserver();
250
+ const observer = new IntersectionObserver((entries) => {
251
+ try {
252
+ entries
253
+ .filter((entry) => entry.isIntersecting)
254
+ .forEach((entry) => {
255
+ observer.unobserve(entry.target); // observe only once
256
+ const target = targetselector(entry.target);
257
+ const source = sourceselector(entry.target);
258
+ sampleRUM(checkpoint, { target, source });
259
+ });
260
+ } catch (error) {
261
+ // something went wrong
262
+ }
263
+ }, { threshold: 0.25 });
264
+ return observer;
265
+ }
266
+ function addViewBlockTracking(element) {
267
+ const blockobserver = getIntersectionObsever('viewblock');
268
+ if (blockobserver) {
269
+ const blocks = element.getAttribute('data-block-status') ? [element] : element.querySelectorAll('div[data-block-status="loaded"]');
270
+ blocks.forEach((b) => blockobserver.observe(b));
271
+ }
272
+ }
273
+
274
+ function addViewMediaTracking(parent) {
275
+ const mediaobserver = getIntersectionObsever('viewmedia');
276
+ if (mediaobserver) {
277
+ parent.querySelectorAll('img, video, audio, iframe').forEach((m) => {
278
+ if (!m.closest('div .block') || m.closest('div[data-block-status="loaded"]')) {
279
+ mediaobserver.observe(m);
280
+ }
281
+ });
282
+ }
283
+ }
284
+
285
+ function addUTMParametersTracking() {
286
+ const usp = new URLSearchParams(window.location.search);
287
+ [...usp.entries()]
288
+ .filter(([key]) => key.startsWith('utm_'))
289
+ // exclude keys that may leak PII
290
+ .filter(([key]) => key !== 'utm_id')
291
+ .filter(([key]) => key !== 'utm_term')
292
+ .forEach(([source, target]) => sampleRUM('utm', { source, target }));
293
+ }
294
+
295
+ function addAdsParametersTracking() {
296
+ const networks = {
297
+ google: /gclid|gclsrc|wbraid|gbraid/,
298
+ doubleclick: /dclid/,
299
+ microsoft: /msclkid/,
300
+ facebook: /fb(cl|ad_|pxl_)id/,
301
+ twitter: /tw(clid|src|term)/,
302
+ linkedin: /li_fat_id/,
303
+ pinterest: /epik/,
304
+ tiktok: /ttclid/,
305
+ };
306
+ const params = Array.from(new URLSearchParams(window.location.search).keys());
307
+ Object.entries(networks).forEach(([network, regex]) => {
308
+ params.filter((param) => regex.test(param)).forEach((param) => sampleRUM('paid', { source: network, target: param }));
309
+ });
310
+ }
311
+
312
+ function addEmailParameterTracking() {
313
+ const networks = {
314
+ mailchimp: /mc_(c|e)id/,
315
+ marketo: /mkt_tok/,
316
+
317
+ };
318
+ const params = Array.from(new URLSearchParams(window.location.search).keys());
319
+ Object.entries(networks).forEach(([network, regex]) => {
320
+ params.filter((param) => regex.test(param)).forEach((param) => sampleRUM('email', { source: network, target: param }));
321
+ });
322
+ }
323
+
324
+ function addFormTracking(parent) {
325
+ activateBlocksMutationObserver();
326
+ parent.querySelectorAll('form').forEach((form) => {
327
+ form.removeEventListener('submit', formSubmitListener); // listen only once
328
+ form.addEventListener('submit', formSubmitListener);
329
+ });
330
+ }
331
+
332
+ function addCookieConsentTracking() {
333
+ const cmpCookie = document.cookie.split(';')
334
+ .map((c) => c.trim())
335
+ .find((cookie) => cookie.startsWith('OptanonAlertBoxClosed='));
336
+
337
+ if (cmpCookie) {
338
+ sampleRUM('consent', { source: 'onetrust', target: 'hidden' });
339
+ return;
340
+ }
341
+
342
+ let consentMutationObserver;
343
+ const trackShowConsent = () => {
344
+ if (document.querySelector('body > div#onetrust-consent-sdk')) {
345
+ sampleRUM('consent', { source: 'onetrust', target: 'show' });
346
+ if (consentMutationObserver) {
347
+ consentMutationObserver.disconnect();
348
+ }
349
+ return true;
350
+ }
351
+ return false;
352
+ };
353
+
354
+ if (!trackShowConsent()) {
355
+ // eslint-disable-next-line max-len
356
+ consentMutationObserver = window.MutationObserver ? new MutationObserver(trackShowConsent) : null;
357
+ if (consentMutationObserver) {
358
+ consentMutationObserver.observe(
359
+ document.body,
360
+ // eslint-disable-next-line object-curly-newline
361
+ { attributes: false, childList: true, subtree: false },
362
+ );
363
+ }
364
+ }
365
+ }
366
+
367
+ const addObserver = (ck, fn, block) => getCollectionConfig().includes(ck) && fn(block);
368
+ function mutationsCallback(mutations) {
369
+ mutations.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
370
+ .filter((m) => m.target.dataset.blockStatus === 'loaded')
371
+ .forEach((m) => {
372
+ addObserver('form', addFormTracking, m.target);
373
+ addObserver('viewblock', addViewBlockTracking, m.target);
374
+ addObserver('viewmedia', addViewMediaTracking, m.target);
375
+ });
376
+ }
377
+
378
+ function addTrackingFromConfig() {
379
+ const trackingFunctions = {
380
+ click: () => {
381
+ document.addEventListener('click', (event) => {
382
+ sampleRUM('click', { target: targetselector(event.target), source: sourceselector(event.target) });
383
+ });
384
+ },
385
+ cwv: () => addCWVTracking(),
386
+ form: () => addFormTracking(window.document.body),
387
+ enterleave: () => addEnterLeaveTracking(),
388
+ loadresource: () => addLoadResourceTracking(),
389
+ utm: () => addUTMParametersTracking(),
390
+ viewblock: () => addViewBlockTracking(window.document.body),
391
+ viewmedia: () => addViewMediaTracking(window.document.body),
392
+ consent: () => addCookieConsentTracking(),
393
+ paid: () => addAdsParametersTracking(),
394
+ email: () => addEmailParameterTracking(),
395
+ };
396
+
397
+ getCollectionConfig().filter((ck) => trackingFunctions[ck])
398
+ .forEach((ck) => trackingFunctions[ck]());
399
+ }
400
+
401
+ function initEnhancer() {
402
+ try {
403
+ if (sampleRUM) {
404
+ addTrackingFromConfig();
405
+ window.hlx.rum.collector = trackCheckpoint;
406
+ processQueue();
407
+ }
408
+ } catch (error) {
409
+ // something went wrong
410
+ }
411
+ }
412
+
413
+ // eslint-disable-next-line padded-blocks
414
+ initEnhancer();
415
+
416
+ })();
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@adobe/helix-rum-enhancer",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Helix RUM Enhancer",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "test": "web-test-runner --node--resolve --coverage",
9
- "test:ci": "web-test-runner --node-resolve --coverage --playwright",
8
+ "setup-playwright": "npx playwright install --with-deps chromium firefox webkit",
9
+ "test": "npm run setup-playwright && web-test-runner --node-resolve --coverage --playwright --browsers chromium firefox webkit",
10
+ "test:ci": "npm run setup-playwright && web-test-runner --node-resolve --coverage --playwright --browsers chromium firefox webkit",
10
11
  "test:watch": "web-test-runner --node-resolve --coverage --watch",
11
12
  "lint": "eslint .",
12
13
  "docs": "npx jsdoc2md -c .jsdoc.json --files 'src/*.js' > docs/API.md",
14
+ "build-bundle": "rollup --config",
15
+ "prepack": "npm run build-bundle",
13
16
  "semantic-release": "semantic-release",
14
17
  "semantic-release-dry": "semantic-release --dry-run --branches $CI_BRANCH 1.x main",
15
18
  "prepare": "husky install"
@@ -37,10 +40,10 @@
37
40
  "@semantic-release/git": "10.0.1",
38
41
  "@semantic-release/npm": "12.0.1",
39
42
  "@web/test-runner": "0.18.2",
40
- "@web/test-runner-core": "0.13.2",
43
+ "@web/test-runner-commands": "0.9.0",
41
44
  "@web/test-runner-mocha": "0.9.0",
42
45
  "@web/test-runner-playwright": "0.11.0",
43
- "c8": "9.1.0",
46
+ "c8": "10.0.0",
44
47
  "codecov": "3.8.3",
45
48
  "eslint": "8.57.0",
46
49
  "eslint-plugin-header": "3.1.1",
@@ -51,6 +54,8 @@
51
54
  "lint-staged": "15.2.5",
52
55
  "mocha": "10.4.0",
53
56
  "mocha-multi-reporters": "1.5.1",
57
+ "rollup": "4.18.0",
58
+ "rollup-plugin-cleanup": "3.2.1",
54
59
  "semantic-release": "24.0.0"
55
60
  },
56
61
  "lint-staged": {
@@ -0,0 +1,54 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import cleanup from 'rollup-plugin-cleanup';
14
+
15
+ const banner = `/*
16
+ * Copyright 2024 Adobe. All rights reserved.
17
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
18
+ * you may not use this file except in compliance with the License. You may obtain a copy
19
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
20
+ *
21
+ * Unless required by applicable law or agreed to in writing, software distributed under
22
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
23
+ * OF ANY KIND, either express or implied. See the License for the specific language
24
+ * governing permissions and limitations under the License.
25
+ */
26
+
27
+ /* eslint-disable max-classes-per-file, wrap-iife */
28
+ // eslint-disable-next-line func-names`;
29
+
30
+ const bundles = [
31
+ {
32
+ source: 'src/index.js',
33
+ outputFile: 'dist/rum-enhancer',
34
+ },
35
+ ];
36
+
37
+ export default [...bundles.map(({ outputFile, source }) => ({
38
+ input: source,
39
+ output: [
40
+ {
41
+ file: `${outputFile}.js`,
42
+ format: 'iife',
43
+ sourcemap: false,
44
+ exports: 'auto',
45
+ banner,
46
+ },
47
+ ],
48
+ plugins: [
49
+ cleanup({
50
+ comments: ['eslint', 'jsdoc', /^\//, /^\*(?!\sc8\s)(?!\n \* Copyright)/],
51
+ maxEmptyLines: -1,
52
+ }),
53
+ ],
54
+ }))];
package/src/index.js CHANGED
@@ -274,7 +274,7 @@ function addViewBlockTracking(element) {
274
274
  function addViewMediaTracking(parent) {
275
275
  const mediaobserver = getIntersectionObsever('viewmedia');
276
276
  if (mediaobserver) {
277
- parent.querySelectorAll('picture > img, video, audio, iframe').forEach((m) => {
277
+ parent.querySelectorAll('img, video, audio, iframe').forEach((m) => {
278
278
  if (!m.closest('div .block') || m.closest('div[data-block-status="loaded"]')) {
279
279
  mediaobserver.observe(m);
280
280
  }
@@ -410,4 +410,5 @@ function initEnhancer() {
410
410
  }
411
411
  }
412
412
 
413
+ // eslint-disable-next-line padded-blocks
413
414
  initEnhancer();