@dataxamas/tracker-js 2.5.0 → 2.7.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 +54 -0
- package/LICENSE +21 -0
- package/dist/index.cjs +22 -7
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +22 -7
- package/package.json +6 -9
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
## 2.7.0
|
|
2
|
+
|
|
3
|
+
- Web Vitals also collect FCP (First Contentful Paint) and TTFB (Time to First Byte), sent in the same batch as
|
|
4
|
+
LCP, INP and CLS. Needs a Dataxamas server that already accepts both metrics.
|
|
5
|
+
- Session replay pauses the recorder after 1 minute without a gesture and resumes on the next one, in the same
|
|
6
|
+
recording. Limits now match the `dataxamas.js` script: 10 minutes, 15,000 events and 5 MB per recording.
|
|
7
|
+
- Element scroll no longer counts as activity, so an auto-scrolling carousel does not keep the recorder running.
|
|
8
|
+
|
|
9
|
+
## 2.6.0
|
|
10
|
+
|
|
11
|
+
- No code changes; same build as 2.5.0.
|
|
12
|
+
|
|
13
|
+
## 2.5.0
|
|
14
|
+
|
|
15
|
+
- `tracking.identify(userId | null)` links the visitor to your own user id for retention cohorts; the server stores
|
|
16
|
+
only a hash of it.
|
|
17
|
+
|
|
18
|
+
## 2.4.0
|
|
19
|
+
|
|
20
|
+
- Visitors are identified server-side: the SDK no longer creates a session id and stores nothing in the browser
|
|
21
|
+
besides the replay key in `sessionStorage`.
|
|
22
|
+
- Replay sampling is decided by the server.
|
|
23
|
+
|
|
24
|
+
## 2.3.0
|
|
25
|
+
|
|
26
|
+
- UTM attribution: only `utm_*` query params are sent.
|
|
27
|
+
- Web Vitals (LCP, INP, CLS), flushed on `visibilitychange` / `pagehide`.
|
|
28
|
+
- `tracking.track(name, data)` custom events for goals and funnels.
|
|
29
|
+
|
|
30
|
+
## 2.1.3
|
|
31
|
+
|
|
32
|
+
- Tuned the replay idle timeout and recording limit.
|
|
33
|
+
|
|
34
|
+
## 2.1.2
|
|
35
|
+
|
|
36
|
+
- Session replay keeps one recording per tab visit across route changes and reloads, closed after idle time or at the
|
|
37
|
+
size cap.
|
|
38
|
+
- Per-site replay sample rate.
|
|
39
|
+
- Skips automated browsers (`navigator.webdriver`).
|
|
40
|
+
|
|
41
|
+
## 2.1.0
|
|
42
|
+
|
|
43
|
+
- Crawler tracking: `DataxamasCrawlers` (`trackRequest`, `track`), `isCrawlerUserAgent`, `withCrawlerTracking` and
|
|
44
|
+
`createExpressCrawlerMiddleware`.
|
|
45
|
+
- Default base URL is now `https://dataxamas.com`.
|
|
46
|
+
|
|
47
|
+
## 2.0.1
|
|
48
|
+
|
|
49
|
+
- Packaging fix, no code changes.
|
|
50
|
+
|
|
51
|
+
## 2.0.0
|
|
52
|
+
|
|
53
|
+
- Renamed to `@dataxamas/tracker-js` (was `dataxamas`).
|
|
54
|
+
- `apiKey` replaces `websiteId`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dataxamas
|
|
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.
|
package/dist/index.cjs
CHANGED
|
@@ -314,21 +314,25 @@ var DataxamasLogs = class {
|
|
|
314
314
|
}
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
+
// ../tracker-shared/src/constants.ts
|
|
318
|
+
var MAX_RECORDING_MS = 10 * 60 * 1e3;
|
|
319
|
+
var MAX_EVENTS_PER_RECORDING = 15e3;
|
|
320
|
+
var MAX_BYTES_PER_RECORDING = 5 * 1024 * 1024;
|
|
321
|
+
var REPLAY_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
322
|
+
var REPLAY_IDLE_SUSPEND_MS = 60 * 1e3;
|
|
323
|
+
|
|
317
324
|
// src/tracking.ts
|
|
318
325
|
var QUEUE_DELAY_MS = 4e3;
|
|
319
326
|
var STORAGE_PREFIX = "@dataxamas";
|
|
320
327
|
var REPLAY_FLUSH_SIZE = 100;
|
|
321
328
|
var REPLAY_FLUSH_INTERVAL_MS = 5e3;
|
|
322
329
|
var CAP_CHECK_MS = 1e3;
|
|
323
|
-
var MAX_RECORDING_MS = 30 * 60 * 1e3;
|
|
324
|
-
var MAX_EVENTS_PER_RECORDING = 5e4;
|
|
325
|
-
var REPLAY_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
326
330
|
var ACTIVITY_SAVE_THROTTLE_MS = 5e3;
|
|
327
331
|
var REPLAY_RECORDING_KEY = `${STORAGE_PREFIX}/replay-recording`;
|
|
328
332
|
function isStoredRecording(value) {
|
|
329
333
|
if (typeof value !== "object" || value === null) return false;
|
|
330
334
|
const stored = value;
|
|
331
|
-
return typeof stored.id === "string" && typeof stored.startedAt === "string" && typeof stored.startedAtMs === "number" && typeof stored.seq === "number" && typeof stored.events === "number" && typeof stored.lastActivityAt === "number" && typeof stored.capped === "boolean";
|
|
335
|
+
return typeof stored.id === "string" && typeof stored.startedAt === "string" && typeof stored.startedAtMs === "number" && typeof stored.seq === "number" && typeof stored.events === "number" && typeof stored.bytes === "number" && typeof stored.lastActivityAt === "number" && typeof stored.capped === "boolean";
|
|
332
336
|
}
|
|
333
337
|
var KEEPALIVE_LIMIT_BYTES = 60 * 1024;
|
|
334
338
|
var DataxamasTracking = class {
|
|
@@ -381,6 +385,7 @@ var DataxamasTracking = class {
|
|
|
381
385
|
this.recordingStartedAtMs = 0;
|
|
382
386
|
this.recordingSeq = 0;
|
|
383
387
|
this.recordingEvents = 0;
|
|
388
|
+
this.recordingBytes = 0;
|
|
384
389
|
this.lastActivityAt = 0;
|
|
385
390
|
this.lastActivitySavedAt = 0;
|
|
386
391
|
}
|
|
@@ -571,6 +576,8 @@ var DataxamasTracking = class {
|
|
|
571
576
|
webVitals.onLCP(queue);
|
|
572
577
|
webVitals.onINP(queue);
|
|
573
578
|
webVitals.onCLS(queue);
|
|
579
|
+
webVitals.onFCP(queue);
|
|
580
|
+
webVitals.onTTFB(queue);
|
|
574
581
|
document.addEventListener("visibilitychange", () => {
|
|
575
582
|
if (document.visibilityState === "hidden") this.flushVitals();
|
|
576
583
|
});
|
|
@@ -589,7 +596,7 @@ var DataxamasTracking = class {
|
|
|
589
596
|
return Boolean(this.stopRrweb);
|
|
590
597
|
}
|
|
591
598
|
exceededReplayCaps() {
|
|
592
|
-
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
599
|
+
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || this.recordingBytes >= MAX_BYTES_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
593
600
|
}
|
|
594
601
|
loadRecording() {
|
|
595
602
|
try {
|
|
@@ -607,6 +614,7 @@ var DataxamasTracking = class {
|
|
|
607
614
|
startedAtMs: this.recordingStartedAtMs,
|
|
608
615
|
seq: this.recordingSeq,
|
|
609
616
|
events: this.recordingEvents,
|
|
617
|
+
bytes: this.recordingBytes,
|
|
610
618
|
lastActivityAt: this.lastActivityAt,
|
|
611
619
|
capped: this.replayCapped
|
|
612
620
|
};
|
|
@@ -622,6 +630,7 @@ var DataxamasTracking = class {
|
|
|
622
630
|
this.recordingStartedAtMs = Date.now();
|
|
623
631
|
this.recordingSeq = 0;
|
|
624
632
|
this.recordingEvents = 0;
|
|
633
|
+
this.recordingBytes = 0;
|
|
625
634
|
this.lastActivityAt = Date.now();
|
|
626
635
|
this.saveRecording();
|
|
627
636
|
}
|
|
@@ -638,6 +647,7 @@ var DataxamasTracking = class {
|
|
|
638
647
|
this.recordingStartedAtMs = stored.startedAtMs;
|
|
639
648
|
this.recordingSeq = stored.seq;
|
|
640
649
|
this.recordingEvents = stored.events;
|
|
650
|
+
this.recordingBytes = stored.bytes;
|
|
641
651
|
this.lastActivityAt = stored.lastActivityAt;
|
|
642
652
|
}
|
|
643
653
|
touchActivity(now) {
|
|
@@ -659,6 +669,7 @@ var DataxamasTracking = class {
|
|
|
659
669
|
...this.measureReplay(),
|
|
660
670
|
events
|
|
661
671
|
});
|
|
672
|
+
this.recordingBytes += payload.length;
|
|
662
673
|
this.saveRecording();
|
|
663
674
|
void this.sendReplayBatch(payload, urgent);
|
|
664
675
|
}
|
|
@@ -732,7 +743,7 @@ var DataxamasTracking = class {
|
|
|
732
743
|
setupReplayLifecycle() {
|
|
733
744
|
if (this.lifecycleReady) return;
|
|
734
745
|
this.lifecycleReady = true;
|
|
735
|
-
for (const type of ["pointerdown", "keydown", "wheel", "touchstart"
|
|
746
|
+
for (const type of ["pointerdown", "keydown", "wheel", "touchstart"]) {
|
|
736
747
|
window.addEventListener(type, this.markReplayActivity, { passive: true, capture: true });
|
|
737
748
|
}
|
|
738
749
|
setInterval(() => {
|
|
@@ -740,7 +751,11 @@ var DataxamasTracking = class {
|
|
|
740
751
|
}, REPLAY_FLUSH_INTERVAL_MS);
|
|
741
752
|
setInterval(() => {
|
|
742
753
|
if (!this.isRecording()) return;
|
|
743
|
-
if (this.exceededReplayCaps())
|
|
754
|
+
if (this.exceededReplayCaps()) {
|
|
755
|
+
this.scheduleReplayCutoff();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (Date.now() - this.lastActivityAt >= REPLAY_IDLE_SUSPEND_MS) this.stopReplayRecorder(false);
|
|
744
759
|
}, CAP_CHECK_MS);
|
|
745
760
|
window.addEventListener("pagehide", () => this.stopReplayRecorder(true));
|
|
746
761
|
document.addEventListener("visibilitychange", () => {
|
package/dist/index.d.cts
CHANGED
|
@@ -134,6 +134,7 @@ declare class DataxamasTracking {
|
|
|
134
134
|
private recordingStartedAtMs;
|
|
135
135
|
private recordingSeq;
|
|
136
136
|
private recordingEvents;
|
|
137
|
+
private recordingBytes;
|
|
137
138
|
private lastActivityAt;
|
|
138
139
|
private lastActivitySavedAt;
|
|
139
140
|
constructor(options: DataxamasTrackingOptions);
|
package/dist/index.d.ts
CHANGED
|
@@ -134,6 +134,7 @@ declare class DataxamasTracking {
|
|
|
134
134
|
private recordingStartedAtMs;
|
|
135
135
|
private recordingSeq;
|
|
136
136
|
private recordingEvents;
|
|
137
|
+
private recordingBytes;
|
|
137
138
|
private lastActivityAt;
|
|
138
139
|
private lastActivitySavedAt;
|
|
139
140
|
constructor(options: DataxamasTrackingOptions);
|
package/dist/index.js
CHANGED
|
@@ -314,21 +314,25 @@ var DataxamasLogs = class {
|
|
|
314
314
|
}
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
+
// ../tracker-shared/src/constants.ts
|
|
318
|
+
var MAX_RECORDING_MS = 10 * 60 * 1e3;
|
|
319
|
+
var MAX_EVENTS_PER_RECORDING = 15e3;
|
|
320
|
+
var MAX_BYTES_PER_RECORDING = 5 * 1024 * 1024;
|
|
321
|
+
var REPLAY_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
322
|
+
var REPLAY_IDLE_SUSPEND_MS = 60 * 1e3;
|
|
323
|
+
|
|
317
324
|
// src/tracking.ts
|
|
318
325
|
var QUEUE_DELAY_MS = 4e3;
|
|
319
326
|
var STORAGE_PREFIX = "@dataxamas";
|
|
320
327
|
var REPLAY_FLUSH_SIZE = 100;
|
|
321
328
|
var REPLAY_FLUSH_INTERVAL_MS = 5e3;
|
|
322
329
|
var CAP_CHECK_MS = 1e3;
|
|
323
|
-
var MAX_RECORDING_MS = 30 * 60 * 1e3;
|
|
324
|
-
var MAX_EVENTS_PER_RECORDING = 5e4;
|
|
325
|
-
var REPLAY_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
326
330
|
var ACTIVITY_SAVE_THROTTLE_MS = 5e3;
|
|
327
331
|
var REPLAY_RECORDING_KEY = `${STORAGE_PREFIX}/replay-recording`;
|
|
328
332
|
function isStoredRecording(value) {
|
|
329
333
|
if (typeof value !== "object" || value === null) return false;
|
|
330
334
|
const stored = value;
|
|
331
|
-
return typeof stored.id === "string" && typeof stored.startedAt === "string" && typeof stored.startedAtMs === "number" && typeof stored.seq === "number" && typeof stored.events === "number" && typeof stored.lastActivityAt === "number" && typeof stored.capped === "boolean";
|
|
335
|
+
return typeof stored.id === "string" && typeof stored.startedAt === "string" && typeof stored.startedAtMs === "number" && typeof stored.seq === "number" && typeof stored.events === "number" && typeof stored.bytes === "number" && typeof stored.lastActivityAt === "number" && typeof stored.capped === "boolean";
|
|
332
336
|
}
|
|
333
337
|
var KEEPALIVE_LIMIT_BYTES = 60 * 1024;
|
|
334
338
|
var DataxamasTracking = class {
|
|
@@ -381,6 +385,7 @@ var DataxamasTracking = class {
|
|
|
381
385
|
this.recordingStartedAtMs = 0;
|
|
382
386
|
this.recordingSeq = 0;
|
|
383
387
|
this.recordingEvents = 0;
|
|
388
|
+
this.recordingBytes = 0;
|
|
384
389
|
this.lastActivityAt = 0;
|
|
385
390
|
this.lastActivitySavedAt = 0;
|
|
386
391
|
}
|
|
@@ -571,6 +576,8 @@ var DataxamasTracking = class {
|
|
|
571
576
|
webVitals.onLCP(queue);
|
|
572
577
|
webVitals.onINP(queue);
|
|
573
578
|
webVitals.onCLS(queue);
|
|
579
|
+
webVitals.onFCP(queue);
|
|
580
|
+
webVitals.onTTFB(queue);
|
|
574
581
|
document.addEventListener("visibilitychange", () => {
|
|
575
582
|
if (document.visibilityState === "hidden") this.flushVitals();
|
|
576
583
|
});
|
|
@@ -589,7 +596,7 @@ var DataxamasTracking = class {
|
|
|
589
596
|
return Boolean(this.stopRrweb);
|
|
590
597
|
}
|
|
591
598
|
exceededReplayCaps() {
|
|
592
|
-
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
599
|
+
return this.recordingEvents >= MAX_EVENTS_PER_RECORDING || this.recordingBytes >= MAX_BYTES_PER_RECORDING || Date.now() - this.recordingStartedAtMs >= MAX_RECORDING_MS;
|
|
593
600
|
}
|
|
594
601
|
loadRecording() {
|
|
595
602
|
try {
|
|
@@ -607,6 +614,7 @@ var DataxamasTracking = class {
|
|
|
607
614
|
startedAtMs: this.recordingStartedAtMs,
|
|
608
615
|
seq: this.recordingSeq,
|
|
609
616
|
events: this.recordingEvents,
|
|
617
|
+
bytes: this.recordingBytes,
|
|
610
618
|
lastActivityAt: this.lastActivityAt,
|
|
611
619
|
capped: this.replayCapped
|
|
612
620
|
};
|
|
@@ -622,6 +630,7 @@ var DataxamasTracking = class {
|
|
|
622
630
|
this.recordingStartedAtMs = Date.now();
|
|
623
631
|
this.recordingSeq = 0;
|
|
624
632
|
this.recordingEvents = 0;
|
|
633
|
+
this.recordingBytes = 0;
|
|
625
634
|
this.lastActivityAt = Date.now();
|
|
626
635
|
this.saveRecording();
|
|
627
636
|
}
|
|
@@ -638,6 +647,7 @@ var DataxamasTracking = class {
|
|
|
638
647
|
this.recordingStartedAtMs = stored.startedAtMs;
|
|
639
648
|
this.recordingSeq = stored.seq;
|
|
640
649
|
this.recordingEvents = stored.events;
|
|
650
|
+
this.recordingBytes = stored.bytes;
|
|
641
651
|
this.lastActivityAt = stored.lastActivityAt;
|
|
642
652
|
}
|
|
643
653
|
touchActivity(now) {
|
|
@@ -659,6 +669,7 @@ var DataxamasTracking = class {
|
|
|
659
669
|
...this.measureReplay(),
|
|
660
670
|
events
|
|
661
671
|
});
|
|
672
|
+
this.recordingBytes += payload.length;
|
|
662
673
|
this.saveRecording();
|
|
663
674
|
void this.sendReplayBatch(payload, urgent);
|
|
664
675
|
}
|
|
@@ -732,7 +743,7 @@ var DataxamasTracking = class {
|
|
|
732
743
|
setupReplayLifecycle() {
|
|
733
744
|
if (this.lifecycleReady) return;
|
|
734
745
|
this.lifecycleReady = true;
|
|
735
|
-
for (const type of ["pointerdown", "keydown", "wheel", "touchstart"
|
|
746
|
+
for (const type of ["pointerdown", "keydown", "wheel", "touchstart"]) {
|
|
736
747
|
window.addEventListener(type, this.markReplayActivity, { passive: true, capture: true });
|
|
737
748
|
}
|
|
738
749
|
setInterval(() => {
|
|
@@ -740,7 +751,11 @@ var DataxamasTracking = class {
|
|
|
740
751
|
}, REPLAY_FLUSH_INTERVAL_MS);
|
|
741
752
|
setInterval(() => {
|
|
742
753
|
if (!this.isRecording()) return;
|
|
743
|
-
if (this.exceededReplayCaps())
|
|
754
|
+
if (this.exceededReplayCaps()) {
|
|
755
|
+
this.scheduleReplayCutoff();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (Date.now() - this.lastActivityAt >= REPLAY_IDLE_SUSPEND_MS) this.stopReplayRecorder(false);
|
|
744
759
|
}, CAP_CHECK_MS);
|
|
745
760
|
window.addEventListener("pagehide", () => this.stopReplayRecorder(true));
|
|
746
761
|
document.addEventListener("visibilitychange", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataxamas/tracker-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Node.js SDK for Dataxamas Analytics API",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"module": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"CHANGELOG.md"
|
|
18
19
|
],
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
@@ -26,14 +27,11 @@
|
|
|
26
27
|
"types": "./dist/index.d.cts",
|
|
27
28
|
"default": "./dist/index.cjs"
|
|
28
29
|
}
|
|
29
|
-
}
|
|
30
|
-
"./script": "./src/script/bundle.ts",
|
|
31
|
-
"./script/vite": "./src/script/vite.ts",
|
|
32
|
-
"./script/virtual": "./src/script/virtual.d.ts"
|
|
30
|
+
}
|
|
33
31
|
},
|
|
34
32
|
"scripts": {
|
|
35
33
|
"build": "tsup",
|
|
36
|
-
"types": "tsc --noEmit
|
|
34
|
+
"types": "tsc --noEmit",
|
|
37
35
|
"lint": "biome lint",
|
|
38
36
|
"lint:fix": "biome lint --write",
|
|
39
37
|
"check": "biome check",
|
|
@@ -48,13 +46,12 @@
|
|
|
48
46
|
],
|
|
49
47
|
"devDependencies": {
|
|
50
48
|
"@dataxamas/biome-config": "workspace:*",
|
|
49
|
+
"@dataxamas/tracker-shared": "workspace:*",
|
|
51
50
|
"@dataxamas/tsconfig": "workspace:*",
|
|
52
51
|
"@types/node": "^26.1.0",
|
|
53
|
-
"esbuild": "^0.28.1",
|
|
54
52
|
"rrweb": "^2.1.0",
|
|
55
53
|
"tsup": "^8.5.1",
|
|
56
54
|
"typescript": "^5",
|
|
57
|
-
"vite": "^8.0.16",
|
|
58
55
|
"web-vitals": "^6.2.3"
|
|
59
56
|
},
|
|
60
57
|
"dependencies": {
|