@statsig/web-analytics 3.25.0 → 3.25.1
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/package.json +3 -3
- package/src/AutoCapture.d.ts +1 -0
- package/src/AutoCapture.js +17 -11
- package/src/AutoCaptureOptions.d.ts +43 -0
- package/src/ConsoleLogManager.d.ts +3 -2
- package/src/ConsoleLogManager.js +11 -24
- package/src/EngagementManager.d.ts +3 -0
- package/src/EngagementManager.js +9 -0
- package/src/utils/consoleLogsUtils.d.ts +2 -0
- package/src/utils/consoleLogsUtils.js +66 -0
- package/src/utils/eventUtils.js +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statsig/web-analytics",
|
|
3
|
-
"version": "3.25.
|
|
3
|
+
"version": "3.25.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"homepage": "https://github.com/statsig-io/js-client-monorepo",
|
|
6
6
|
"repository": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"directory": "packages/web-analytics"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@statsig/client-core": "3.25.
|
|
13
|
-
"@statsig/js-client": "3.25.
|
|
12
|
+
"@statsig/client-core": "3.25.1",
|
|
13
|
+
"@statsig/js-client": "3.25.1",
|
|
14
14
|
"web-vitals": "5.0.3"
|
|
15
15
|
},
|
|
16
16
|
"jsdelivr": "./build/statsig-web-analytics.min.js",
|
package/src/AutoCapture.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare class AutoCapture {
|
|
|
20
20
|
private _webVitalsManager;
|
|
21
21
|
private _deadClickManager;
|
|
22
22
|
private _consoleLogManager;
|
|
23
|
+
private _options?;
|
|
23
24
|
constructor(_client: PrecomputedEvaluationsInterface, options?: AutoCaptureOptions);
|
|
24
25
|
private _addEventHandlers;
|
|
25
26
|
private _addPageViewTracking;
|
package/src/AutoCapture.js
CHANGED
|
@@ -49,6 +49,7 @@ class AutoCapture {
|
|
|
49
49
|
this._hasLoggedPageViewEnd = false;
|
|
50
50
|
this._pageViewLogged = false;
|
|
51
51
|
const { sdkKey, errorBoundary, values } = _client.getContext();
|
|
52
|
+
this._options = options;
|
|
52
53
|
this._disabledEvents = (_b = (_a = values === null || values === void 0 ? void 0 : values.auto_capture_settings) === null || _a === void 0 ? void 0 : _a.disabled_events) !== null && _b !== void 0 ? _b : {};
|
|
53
54
|
this._errorBoundary = errorBoundary;
|
|
54
55
|
this._errorBoundary.wrap(this, 'autoCapture:');
|
|
@@ -111,23 +112,23 @@ class AutoCapture {
|
|
|
111
112
|
if (!win || !doc) {
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
|
-
(0, commonUtils_1._registerEventHandler)(win, 'popstate', () => this._tryLogPageView());
|
|
115
|
+
(0, commonUtils_1._registerEventHandler)(win, 'popstate', () => this._tryLogPageView('popstate'));
|
|
115
116
|
window.history.pushState = new Proxy(window.history.pushState, {
|
|
116
117
|
apply: (target, thisArg, [state, unused, url]) => {
|
|
117
118
|
target.apply(thisArg, [state, unused, url]);
|
|
118
|
-
this._tryLogPageView();
|
|
119
|
+
this._tryLogPageView('pushstate');
|
|
119
120
|
},
|
|
120
121
|
});
|
|
121
122
|
window.history.replaceState = new Proxy(window.history.replaceState, {
|
|
122
123
|
apply: (target, thisArg, [state, unused, url]) => {
|
|
123
124
|
target.apply(thisArg, [state, unused, url]);
|
|
124
|
-
this._tryLogPageView();
|
|
125
|
+
this._tryLogPageView('replacestate');
|
|
125
126
|
},
|
|
126
127
|
});
|
|
127
128
|
this._tryLogPageView();
|
|
128
129
|
}
|
|
129
130
|
_autoLogEvent(event) {
|
|
130
|
-
var _a, _b, _c;
|
|
131
|
+
var _a, _b, _c, _d;
|
|
131
132
|
const eventType = (_a = event.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
132
133
|
if (eventType === 'error' && event instanceof ErrorEvent) {
|
|
133
134
|
this._logError(event);
|
|
@@ -151,11 +152,16 @@ class AutoCapture {
|
|
|
151
152
|
if (!selectedText) {
|
|
152
153
|
return;
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
+
if ((_d = this._options) === null || _d === void 0 ? void 0 : _d.captureCopyText) {
|
|
156
|
+
metadata['selectedText'] = (0, commonUtils_1._sanitizeString)(selectedText);
|
|
157
|
+
}
|
|
155
158
|
const clipType = event.type || 'clipboard';
|
|
156
159
|
metadata['clipType'] = clipType;
|
|
157
160
|
}
|
|
158
161
|
const { value, metadata: eventMetadata } = (0, eventUtils_1._gatherEventData)(target);
|
|
162
|
+
if (eventMetadata['isOutbound']) {
|
|
163
|
+
this._engagementManager.setMeaningfulEngagementOccurred(true);
|
|
164
|
+
}
|
|
159
165
|
Object.assign(metadata, eventMetadata);
|
|
160
166
|
const allMetadata = (0, metadataUtils_1._gatherAllMetadata)((0, commonUtils_1._getSafeUrl)());
|
|
161
167
|
this._enqueueAutoCapture(eventName, value, Object.assign(Object.assign({}, allMetadata), metadata));
|
|
@@ -204,15 +210,18 @@ class AutoCapture {
|
|
|
204
210
|
this._errorBoundary.logError('AC::logSession', err);
|
|
205
211
|
}
|
|
206
212
|
}
|
|
207
|
-
_tryLogPageView() {
|
|
213
|
+
_tryLogPageView(navigationType) {
|
|
208
214
|
const url = (0, commonUtils_1._getSafeUrl)();
|
|
209
215
|
const last = this._previousLoggedPageViewUrl;
|
|
210
|
-
if (last && url.
|
|
216
|
+
if (last && url.pathname === last.pathname) {
|
|
211
217
|
return;
|
|
212
218
|
}
|
|
213
219
|
this._engagementManager.setLastPageViewTime(Date.now());
|
|
214
220
|
this._hasLoggedPageViewEnd = false;
|
|
215
221
|
const payload = (0, metadataUtils_1._gatherAllMetadata)(url);
|
|
222
|
+
if (navigationType) {
|
|
223
|
+
payload['navigation_type'] = navigationType;
|
|
224
|
+
}
|
|
216
225
|
if (this._previousLoggedPageViewUrl) {
|
|
217
226
|
payload['last_page_view_url'] = this._previousLoggedPageViewUrl.href;
|
|
218
227
|
}
|
|
@@ -232,10 +241,7 @@ class AutoCapture {
|
|
|
232
241
|
return;
|
|
233
242
|
}
|
|
234
243
|
this._hasLoggedPageViewEnd = true;
|
|
235
|
-
|
|
236
|
-
const pageViewLength = this._engagementManager.getPageViewLength();
|
|
237
|
-
this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW_END, (0, commonUtils_1._getSanitizedPageUrl)(), Object.assign(Object.assign({}, scrollMetrics), { pageViewLength,
|
|
238
|
-
dueToInactivity }), {
|
|
244
|
+
this._enqueueAutoCapture(AutoCaptureEvent_1.AutoCaptureEventName.PAGE_VIEW_END, (0, commonUtils_1._getSanitizedPageUrl)(), Object.assign(Object.assign({}, this._engagementManager.getPageViewEndMetadata()), { dueToInactivity }), {
|
|
239
245
|
flushImmediately: true,
|
|
240
246
|
});
|
|
241
247
|
}
|
|
@@ -1,11 +1,54 @@
|
|
|
1
1
|
import { AutoCaptureEvent } from './AutoCaptureEvent';
|
|
2
2
|
import { ConsoleLogLevel } from './ConsoleLogManager';
|
|
3
3
|
export type AutoCaptureOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Optional function to filter which auto-capture events should be recorded.
|
|
6
|
+
* If provided, this function will be called for each auto-capture event.
|
|
7
|
+
* Return true to capture the event, false to ignore it.
|
|
8
|
+
*
|
|
9
|
+
* @param event - The auto-capture event to evaluate
|
|
10
|
+
* @returns true if the event should be captured, false otherwise
|
|
11
|
+
*/
|
|
4
12
|
eventFilterFunc?: (event: AutoCaptureEvent) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Settings for automatically capturing console log events.
|
|
15
|
+
* When enabled, console.log, console.warn, console.error, etc. calls
|
|
16
|
+
* will be automatically tracked as statsig::log_line events.
|
|
17
|
+
*/
|
|
5
18
|
consoleLogAutoCaptureSettings?: ConsoleLogAutoCaptureSettings;
|
|
19
|
+
/**
|
|
20
|
+
* Whether to capture text content when copy events occur.
|
|
21
|
+
* When true, the actual text being copied will be sanitized and included in copy event data.
|
|
22
|
+
* When false or undefined, copy events will be tracked without the text content.
|
|
23
|
+
*
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
captureCopyText?: boolean;
|
|
6
27
|
};
|
|
7
28
|
export type ConsoleLogAutoCaptureSettings = {
|
|
29
|
+
/**
|
|
30
|
+
* Whether console log auto-capture is enabled.
|
|
31
|
+
* When true, console method calls will be automatically tracked.
|
|
32
|
+
* When false, console events will not be captured.
|
|
33
|
+
*/
|
|
8
34
|
enabled: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The minimum log level to capture.
|
|
37
|
+
* Only console methods at or above this level will be tracked.
|
|
38
|
+
* For example, if set to 'warn', only console.warn and console.error will be captured.
|
|
39
|
+
*
|
|
40
|
+
* @default 'info' - captures info, log, warn, error levels
|
|
41
|
+
*/
|
|
9
42
|
logLevel?: ConsoleLogLevel;
|
|
43
|
+
/**
|
|
44
|
+
* Sampling rate for console log events (0.0 to 1.0).
|
|
45
|
+
* Determines what percentage of console events should be captured.
|
|
46
|
+
* 1.0 means capture all events, 0.5 means capture 50% of events, etc.
|
|
47
|
+
*
|
|
48
|
+
* @default 1.0 - capture all events
|
|
49
|
+
*/
|
|
10
50
|
sampleRate?: number;
|
|
51
|
+
maxKeys?: number;
|
|
52
|
+
maxDepth?: number;
|
|
53
|
+
maxStringLength?: number;
|
|
11
54
|
};
|
|
@@ -10,12 +10,13 @@ export declare class ConsoleLogManager {
|
|
|
10
10
|
private _isTracking;
|
|
11
11
|
private _logLevel;
|
|
12
12
|
private readonly __source;
|
|
13
|
+
private readonly _maxKeys;
|
|
14
|
+
private readonly _maxDepth;
|
|
15
|
+
private readonly _maxStringLength;
|
|
13
16
|
constructor(_enqueueFn: (eventName: AutoCaptureEventName, value: string, metadata: Record<string, unknown>) => void, _errorBoundary: ErrorBoundary, _options: ConsoleLogAutoCaptureSettings);
|
|
14
17
|
startTracking(): void;
|
|
15
18
|
stopTracking(): void;
|
|
16
19
|
private _patchConsole;
|
|
17
20
|
private _enqueueConsoleLog;
|
|
18
21
|
private _shouldLog;
|
|
19
|
-
private _safeStringify;
|
|
20
|
-
private _getStackTrace;
|
|
21
22
|
}
|
package/src/ConsoleLogManager.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.ConsoleLogManager = void 0;
|
|
|
5
5
|
const client_core_1 = require("@statsig/client-core");
|
|
6
6
|
const AutoCaptureEvent_1 = require("./AutoCaptureEvent");
|
|
7
7
|
const commonUtils_1 = require("./utils/commonUtils");
|
|
8
|
+
const consoleLogsUtils_1 = require("./utils/consoleLogsUtils");
|
|
8
9
|
const metadataUtils_1 = require("./utils/metadataUtils");
|
|
9
10
|
const ConsoleLogPriority = {
|
|
10
11
|
trace: 10,
|
|
@@ -14,9 +15,12 @@ const ConsoleLogPriority = {
|
|
|
14
15
|
warn: 40,
|
|
15
16
|
error: 50,
|
|
16
17
|
};
|
|
18
|
+
const DEFAULT_MAX_KEYS = 10;
|
|
19
|
+
const DEFAULT_MAX_DEPTH = 10;
|
|
20
|
+
const DEFAULT_MAX_STRING_LENGTH = 500;
|
|
17
21
|
class ConsoleLogManager {
|
|
18
22
|
constructor(_enqueueFn, _errorBoundary, _options) {
|
|
19
|
-
var _a;
|
|
23
|
+
var _a, _b, _c, _d;
|
|
20
24
|
this._enqueueFn = _enqueueFn;
|
|
21
25
|
this._errorBoundary = _errorBoundary;
|
|
22
26
|
this._options = _options;
|
|
@@ -25,6 +29,10 @@ class ConsoleLogManager {
|
|
|
25
29
|
this._logLevel = 'info';
|
|
26
30
|
this.__source = 'js-auto-capture';
|
|
27
31
|
this._logLevel = (_a = this._options.logLevel) !== null && _a !== void 0 ? _a : 'info';
|
|
32
|
+
this._maxKeys = (_b = this._options.maxKeys) !== null && _b !== void 0 ? _b : DEFAULT_MAX_KEYS;
|
|
33
|
+
this._maxDepth = (_c = this._options.maxDepth) !== null && _c !== void 0 ? _c : DEFAULT_MAX_DEPTH;
|
|
34
|
+
this._maxStringLength =
|
|
35
|
+
(_d = this._options.maxStringLength) !== null && _d !== void 0 ? _d : DEFAULT_MAX_STRING_LENGTH;
|
|
28
36
|
}
|
|
29
37
|
startTracking() {
|
|
30
38
|
try {
|
|
@@ -63,8 +71,8 @@ class ConsoleLogManager {
|
|
|
63
71
|
return;
|
|
64
72
|
inStack = true;
|
|
65
73
|
try {
|
|
66
|
-
const payload = args.map((a) =>
|
|
67
|
-
const trace =
|
|
74
|
+
const payload = args.map((a) => (0, consoleLogsUtils_1._safeStringify)(a, this._maxKeys, this._maxDepth, this._maxStringLength));
|
|
75
|
+
const trace = (0, consoleLogsUtils_1._getStackTrace)();
|
|
68
76
|
this._enqueueConsoleLog(level, payload, trace);
|
|
69
77
|
}
|
|
70
78
|
catch (err) {
|
|
@@ -95,26 +103,5 @@ class ConsoleLogManager {
|
|
|
95
103
|
}
|
|
96
104
|
return Math.random() < this._options.sampleRate;
|
|
97
105
|
}
|
|
98
|
-
_safeStringify(val) {
|
|
99
|
-
try {
|
|
100
|
-
if (typeof val === 'string')
|
|
101
|
-
return val;
|
|
102
|
-
if (typeof val === 'object' && val !== null)
|
|
103
|
-
return JSON.stringify(val);
|
|
104
|
-
return String(val);
|
|
105
|
-
}
|
|
106
|
-
catch (_a) {
|
|
107
|
-
return '[Unserializable]';
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
_getStackTrace() {
|
|
111
|
-
var _a, _b;
|
|
112
|
-
try {
|
|
113
|
-
return (_b = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n').slice(2)) !== null && _b !== void 0 ? _b : [];
|
|
114
|
-
}
|
|
115
|
-
catch (_c) {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
106
|
}
|
|
120
107
|
exports.ConsoleLogManager = ConsoleLogManager;
|
|
@@ -6,6 +6,7 @@ export declare class EngagementManager {
|
|
|
6
6
|
private _lastPageViewTime;
|
|
7
7
|
private _inactiveTimer;
|
|
8
8
|
private _onInactivityCallback;
|
|
9
|
+
private _meaningfulEngagementOccurred;
|
|
9
10
|
constructor();
|
|
10
11
|
private _initializeScrollTracking;
|
|
11
12
|
private _handleScroll;
|
|
@@ -20,4 +21,6 @@ export declare class EngagementManager {
|
|
|
20
21
|
setLastPageViewTime(time: number): void;
|
|
21
22
|
startInactivityTracking(callback: () => void): void;
|
|
22
23
|
bumpInactiveTimer(): void;
|
|
24
|
+
setMeaningfulEngagementOccurred(occurred: boolean): void;
|
|
25
|
+
getPageViewEndMetadata(): Record<string, unknown>;
|
|
23
26
|
}
|
package/src/EngagementManager.js
CHANGED
|
@@ -12,6 +12,7 @@ class EngagementManager {
|
|
|
12
12
|
this._lastPageViewTime = Date.now();
|
|
13
13
|
this._inactiveTimer = null;
|
|
14
14
|
this._onInactivityCallback = null;
|
|
15
|
+
this._meaningfulEngagementOccurred = false;
|
|
15
16
|
this._initializeScrollTracking();
|
|
16
17
|
}
|
|
17
18
|
_initializeScrollTracking() {
|
|
@@ -69,5 +70,13 @@ class EngagementManager {
|
|
|
69
70
|
}
|
|
70
71
|
}, PAGE_INACTIVE_TIMEOUT);
|
|
71
72
|
}
|
|
73
|
+
setMeaningfulEngagementOccurred(occurred) {
|
|
74
|
+
this._meaningfulEngagementOccurred = occurred;
|
|
75
|
+
}
|
|
76
|
+
getPageViewEndMetadata() {
|
|
77
|
+
const pageviewEndMetadata = Object.assign(Object.assign({}, this.getScrollMetrics()), { pageViewLength: this.getPageViewLength(), meaningfulEngagementOccurred: this._meaningfulEngagementOccurred });
|
|
78
|
+
this.setMeaningfulEngagementOccurred(false);
|
|
79
|
+
return pageviewEndMetadata;
|
|
80
|
+
}
|
|
72
81
|
}
|
|
73
82
|
exports.EngagementManager = EngagementManager;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._getStackTrace = exports._safeStringify = void 0;
|
|
4
|
+
function _truncateString(str, maxLength) {
|
|
5
|
+
if (str.length <= maxLength) {
|
|
6
|
+
return str;
|
|
7
|
+
}
|
|
8
|
+
return str.slice(0, maxLength) + '...';
|
|
9
|
+
}
|
|
10
|
+
function _safeStringify(val, maxKeysCount, maxDepth, maxLength) {
|
|
11
|
+
try {
|
|
12
|
+
if (_shouldNotStringify(val, maxKeysCount, maxDepth)) {
|
|
13
|
+
return _simpleStringify(val, maxLength);
|
|
14
|
+
}
|
|
15
|
+
if (typeof val === 'string') {
|
|
16
|
+
return _truncateString(val, maxLength);
|
|
17
|
+
}
|
|
18
|
+
if (typeof val === 'object' && val !== null) {
|
|
19
|
+
return _truncateString(JSON.stringify(val), maxLength);
|
|
20
|
+
}
|
|
21
|
+
return _truncateString(String(val), maxLength);
|
|
22
|
+
}
|
|
23
|
+
catch (_a) {
|
|
24
|
+
return _truncateString('[Unserializable]', maxLength);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports._safeStringify = _safeStringify;
|
|
28
|
+
function _getStackTrace() {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
try {
|
|
31
|
+
return (_b = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split('\n').slice(2)) !== null && _b !== void 0 ? _b : [];
|
|
32
|
+
}
|
|
33
|
+
catch (_c) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports._getStackTrace = _getStackTrace;
|
|
38
|
+
function _shouldNotStringify(val, maxKeysCount, maxDepth) {
|
|
39
|
+
if (_isPlainObject(val)) {
|
|
40
|
+
if (Object.keys(val).length > maxKeysCount) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (_isObjectTooDeep(val, maxDepth)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (typeof val === 'function') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function _isPlainObject(obj) {
|
|
53
|
+
return Object.prototype.toString.call(obj) === '[object Object]';
|
|
54
|
+
}
|
|
55
|
+
function _isObjectTooDeep(obj, maxDepth) {
|
|
56
|
+
if (maxDepth <= 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return Object.keys(obj).some((key) => _isObjectTooDeep(obj[key], maxDepth - 1));
|
|
63
|
+
}
|
|
64
|
+
function _simpleStringify(val, maxLength) {
|
|
65
|
+
return _truncateString(val.toString(), maxLength);
|
|
66
|
+
}
|
package/src/utils/eventUtils.js
CHANGED
|
@@ -30,6 +30,9 @@ function _gatherEventData(target) {
|
|
|
30
30
|
if (tagName === 'button' || anchor) {
|
|
31
31
|
Object.assign(metadata, _getButtonMetadata(anchor || target));
|
|
32
32
|
}
|
|
33
|
+
if (_isOutboundLink(metadata)) {
|
|
34
|
+
metadata['isOutbound'] = 'true';
|
|
35
|
+
}
|
|
33
36
|
return { value, metadata };
|
|
34
37
|
}
|
|
35
38
|
exports._gatherEventData = _gatherEventData;
|
|
@@ -96,6 +99,20 @@ function _truncateString(str, maxLength) {
|
|
|
96
99
|
return null;
|
|
97
100
|
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
|
98
101
|
}
|
|
102
|
+
function _isOutboundLink(metadata) {
|
|
103
|
+
var _a;
|
|
104
|
+
if (Array.isArray(metadata['classList']) &&
|
|
105
|
+
((_a = metadata['classList']) === null || _a === void 0 ? void 0 : _a.includes('statsig-ctr-capture'))) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const href = metadata['href'];
|
|
109
|
+
if (href) {
|
|
110
|
+
const currentUrl = (0, commonUtils_1._getSafeUrl)();
|
|
111
|
+
const linkUrl = new URL(href);
|
|
112
|
+
return currentUrl.host !== linkUrl.host;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
99
116
|
function _getSafeMetadataAttributes(elem) {
|
|
100
117
|
const metadata = {};
|
|
101
118
|
metadata['class'] = _normalizeClassAttribute(_truncateString(elem.getAttribute('class'), MAX_ATTRIBUTE_LENGTH) || '');
|