core-outline 1.1.23 → 1.1.25
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/README.md +34 -11
- package/dist/index.es.js +178 -69
- package/dist/index.js +178 -69
- package/package.json +4 -1
- package/.babelrc +0 -3
- package/.eslintignore +0 -2
- package/.eslintrc.json +0 -23
- package/.prettierignore +0 -2
- package/.prettierrc +0 -7
- package/.storybook/main.js +0 -16
- package/.storybook/preview.js +0 -13
- package/docs/changelog/270626-143000_tracking_pipeline_changelog.md +0 -44
- package/rollup.config.js +0 -35
- package/src/components/CoreOutline/CoreOutline.js +0 -223
- package/src/components/CoreOutline/RWebRecorder.js +0 -26
- package/src/components/CoreOutline/helpers.js +0 -66
- package/src/components/CoreOutline/index.js +0 -1
- package/src/components/CoreOutline/ingest.js +0 -116
- package/src/components/CoreOutline/socket.js +0 -3
- package/src/index.js +0 -1
package/README.md
CHANGED
|
@@ -12,17 +12,6 @@ npm install core-outline
|
|
|
12
12
|
yarn add core-outline
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The response contains your `data_source_id`, `data_source_secret`, and `warehouse_id`. **The secret is shown once — store it securely.**
|
|
16
|
-
|
|
17
|
-
```json
|
|
18
|
-
{
|
|
19
|
-
"data_source_id": "3f4a1c2d-...",
|
|
20
|
-
"data_source_secret": "aB3xK9mZqR...",
|
|
21
|
-
"warehouse_id": "cad1acc0-...",
|
|
22
|
-
"name": "My Website"
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
15
|
## Usage
|
|
27
16
|
|
|
28
17
|
Wrap your app's root component with `<CoreOutline>`. All tracking is automatic from that point — no additional configuration is needed.
|
|
@@ -79,3 +68,37 @@ flag_item_clicked('sku-123');
|
|
|
79
68
|
// Track a purchase (price is optional, maps to value_num in ClickHouse)
|
|
80
69
|
flag_item_purchased('sku-123', 49.99);
|
|
81
70
|
```
|
|
71
|
+
|
|
72
|
+
## Data Pipeline
|
|
73
|
+
|
|
74
|
+
Events flow through the following pipeline:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Browser → POST /api/ingest/events → RabbitMQ ({warehouse_id}.analytics.events)
|
|
78
|
+
→ Consumer worker → ClickHouse atlas_analytics
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The ClickHouse tables populated are:
|
|
82
|
+
|
|
83
|
+
| Table | Populated by |
|
|
84
|
+
|-------|-------------|
|
|
85
|
+
| `fact_event` | All events except session start/end |
|
|
86
|
+
| `fact_session` | `session_start` and `session_end` events |
|
|
87
|
+
| `fact_pageview` | `pageview` events |
|
|
88
|
+
| `stg_events_sdk_event` | All events (raw staging) |
|
|
89
|
+
| `stg_rrweb_event` | Session recording chunks |
|
|
90
|
+
|
|
91
|
+
These tables are queried by the [atlas-analytics](https://analytics.atlas.coreoutline.com) API under `/metrics/saas/traffic`, `/metrics/saas/engagement`, and related endpoints.
|
|
92
|
+
|
|
93
|
+
## Anonymous vs Session Identity
|
|
94
|
+
|
|
95
|
+
- **`anonymous_id`** — a UUID stored in `localStorage`; persists across browser sessions, used to stitch cross-session user journeys.
|
|
96
|
+
- **`session_id`** — a UUID stored in `sessionStorage`; reset on each new tab or browser close, matching the standard session definition.
|
|
97
|
+
|
|
98
|
+
## Service URLs
|
|
99
|
+
|
|
100
|
+
| Service | URL |
|
|
101
|
+
|---------|-----|
|
|
102
|
+
| Hermes API (credential management) | `https://atlas-orchestrator.atlas.coreoutline.com` |
|
|
103
|
+
| Maia (data engineering / pipelines) | `https://data-engineering.atlas.coreoutline.com` |
|
|
104
|
+
| Atlas Analytics API | `https://analytics.atlas.coreoutline.com` |
|
package/dist/index.es.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useState, useRef, useEffect } from 'react';
|
|
2
3
|
import { v4 } from 'uuid';
|
|
3
4
|
|
|
@@ -4411,25 +4412,57 @@ function getPagePath() {
|
|
|
4411
4412
|
|
|
4412
4413
|
var INGEST_BASE = 'https://atlas-orchestrator.atlas.coreoutline.com';
|
|
4413
4414
|
var FLUSH_INTERVAL_MS = 5000;
|
|
4415
|
+
// Refresh the token this many milliseconds before it actually expires.
|
|
4416
|
+
var TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
4417
|
+
|
|
4414
4418
|
var _token = null;
|
|
4415
4419
|
var _warehouseId = null;
|
|
4416
4420
|
var _organizationId = null;
|
|
4417
4421
|
var _dataSourceId = null;
|
|
4422
|
+
var _dataSourceSecret = null;
|
|
4418
4423
|
var _buffer = [];
|
|
4419
4424
|
var _flushInterval = null;
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4425
|
+
var _refreshTimeout = null;
|
|
4426
|
+
function _scheduleTokenRefresh(expiresInSeconds) {
|
|
4427
|
+
if (_refreshTimeout) {
|
|
4428
|
+
clearTimeout(_refreshTimeout);
|
|
4429
|
+
_refreshTimeout = null;
|
|
4430
|
+
}
|
|
4431
|
+
var refreshInMs = expiresInSeconds * 1000 - TOKEN_REFRESH_BUFFER_MS;
|
|
4432
|
+
if (refreshInMs <= 0) {
|
|
4433
|
+
// Token already near expiry — refresh immediately on next action.
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
_refreshTimeout = setTimeout(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
|
|
4437
|
+
var ok;
|
|
4426
4438
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
4427
4439
|
while (1) switch (_context.prev = _context.next) {
|
|
4428
4440
|
case 0:
|
|
4429
|
-
|
|
4430
|
-
_dataSourceId
|
|
4431
|
-
|
|
4432
|
-
|
|
4441
|
+
_context.next = 2;
|
|
4442
|
+
return _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
4443
|
+
case 2:
|
|
4444
|
+
ok = _context.sent;
|
|
4445
|
+
if (!ok) {
|
|
4446
|
+
console.warn('[CoreOutline] Proactive token refresh failed; will retry on next flush.');
|
|
4447
|
+
}
|
|
4448
|
+
case 4:
|
|
4449
|
+
case "end":
|
|
4450
|
+
return _context.stop();
|
|
4451
|
+
}
|
|
4452
|
+
}, _callee);
|
|
4453
|
+
})), refreshInMs);
|
|
4454
|
+
}
|
|
4455
|
+
function _doAuthorize(_x, _x2, _x3) {
|
|
4456
|
+
return _doAuthorize2.apply(this, arguments);
|
|
4457
|
+
}
|
|
4458
|
+
function _doAuthorize2() {
|
|
4459
|
+
_doAuthorize2 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(warehouseId, dataSourceId, dataSourceSecret) {
|
|
4460
|
+
var res, data, expiresIn;
|
|
4461
|
+
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
|
|
4462
|
+
while (1) switch (_context2.prev = _context2.next) {
|
|
4463
|
+
case 0:
|
|
4464
|
+
_context2.prev = 0;
|
|
4465
|
+
_context2.next = 3;
|
|
4433
4466
|
return fetch("".concat(INGEST_BASE, "/api/ingest/authorize"), {
|
|
4434
4467
|
method: 'POST',
|
|
4435
4468
|
headers: {
|
|
@@ -4441,35 +4474,85 @@ function _initIngest() {
|
|
|
4441
4474
|
data_source_secret: dataSourceSecret
|
|
4442
4475
|
})
|
|
4443
4476
|
});
|
|
4444
|
-
case
|
|
4445
|
-
res =
|
|
4477
|
+
case 3:
|
|
4478
|
+
res = _context2.sent;
|
|
4446
4479
|
if (res.ok) {
|
|
4447
|
-
|
|
4480
|
+
_context2.next = 7;
|
|
4448
4481
|
break;
|
|
4449
4482
|
}
|
|
4450
4483
|
console.error('[CoreOutline] Authorization failed:', res.status);
|
|
4451
|
-
return
|
|
4452
|
-
case
|
|
4453
|
-
|
|
4484
|
+
return _context2.abrupt("return", false);
|
|
4485
|
+
case 7:
|
|
4486
|
+
_context2.next = 9;
|
|
4454
4487
|
return res.json();
|
|
4455
|
-
case
|
|
4456
|
-
data =
|
|
4488
|
+
case 9:
|
|
4489
|
+
data = _context2.sent;
|
|
4457
4490
|
_token = data.access_token;
|
|
4458
4491
|
_organizationId = data.organization_id;
|
|
4459
|
-
|
|
4492
|
+
expiresIn = data.expires_in || 24 * 3600;
|
|
4493
|
+
_scheduleTokenRefresh(expiresIn);
|
|
4494
|
+
return _context2.abrupt("return", true);
|
|
4460
4495
|
case 17:
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
console.error('[CoreOutline] Authorization error:',
|
|
4464
|
-
return
|
|
4496
|
+
_context2.prev = 17;
|
|
4497
|
+
_context2.t0 = _context2["catch"](0);
|
|
4498
|
+
console.error('[CoreOutline] Authorization error:', _context2.t0);
|
|
4499
|
+
return _context2.abrupt("return", false);
|
|
4465
4500
|
case 21:
|
|
4466
4501
|
case "end":
|
|
4467
|
-
return
|
|
4502
|
+
return _context2.stop();
|
|
4503
|
+
}
|
|
4504
|
+
}, _callee2, null, [[0, 17]]);
|
|
4505
|
+
}));
|
|
4506
|
+
return _doAuthorize2.apply(this, arguments);
|
|
4507
|
+
}
|
|
4508
|
+
function initIngest(_x4, _x5, _x6) {
|
|
4509
|
+
return _initIngest.apply(this, arguments);
|
|
4510
|
+
}
|
|
4511
|
+
function _initIngest() {
|
|
4512
|
+
_initIngest = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee3(warehouseId, dataSourceId, dataSourceSecret) {
|
|
4513
|
+
return _regeneratorRuntime().wrap(function _callee3$(_context3) {
|
|
4514
|
+
while (1) switch (_context3.prev = _context3.next) {
|
|
4515
|
+
case 0:
|
|
4516
|
+
_warehouseId = warehouseId;
|
|
4517
|
+
_dataSourceId = dataSourceId;
|
|
4518
|
+
_dataSourceSecret = dataSourceSecret;
|
|
4519
|
+
return _context3.abrupt("return", _doAuthorize(warehouseId, dataSourceId, dataSourceSecret));
|
|
4520
|
+
case 4:
|
|
4521
|
+
case "end":
|
|
4522
|
+
return _context3.stop();
|
|
4468
4523
|
}
|
|
4469
|
-
},
|
|
4524
|
+
}, _callee3);
|
|
4470
4525
|
}));
|
|
4471
4526
|
return _initIngest.apply(this, arguments);
|
|
4472
4527
|
}
|
|
4528
|
+
function _ensureFreshToken() {
|
|
4529
|
+
return _ensureFreshToken2.apply(this, arguments);
|
|
4530
|
+
}
|
|
4531
|
+
function _ensureFreshToken2() {
|
|
4532
|
+
_ensureFreshToken2 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee4() {
|
|
4533
|
+
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
|
4534
|
+
while (1) switch (_context4.prev = _context4.next) {
|
|
4535
|
+
case 0:
|
|
4536
|
+
if (!_token) {
|
|
4537
|
+
_context4.next = 2;
|
|
4538
|
+
break;
|
|
4539
|
+
}
|
|
4540
|
+
return _context4.abrupt("return");
|
|
4541
|
+
case 2:
|
|
4542
|
+
if (!(_warehouseId && _dataSourceId && _dataSourceSecret)) {
|
|
4543
|
+
_context4.next = 5;
|
|
4544
|
+
break;
|
|
4545
|
+
}
|
|
4546
|
+
_context4.next = 5;
|
|
4547
|
+
return _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
4548
|
+
case 5:
|
|
4549
|
+
case "end":
|
|
4550
|
+
return _context4.stop();
|
|
4551
|
+
}
|
|
4552
|
+
}, _callee4);
|
|
4553
|
+
}));
|
|
4554
|
+
return _ensureFreshToken2.apply(this, arguments);
|
|
4555
|
+
}
|
|
4473
4556
|
function trackEvent(eventType, payload) {
|
|
4474
4557
|
if (!_token) return;
|
|
4475
4558
|
var event = _objectSpread2({
|
|
@@ -4483,49 +4566,75 @@ function trackEvent(eventType, payload) {
|
|
|
4483
4566
|
}
|
|
4484
4567
|
}
|
|
4485
4568
|
function flush() {
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4569
|
+
return _flush.apply(this, arguments);
|
|
4570
|
+
}
|
|
4571
|
+
function _flush() {
|
|
4572
|
+
_flush = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee5() {
|
|
4573
|
+
var events, body, headers, blob;
|
|
4574
|
+
return _regeneratorRuntime().wrap(function _callee5$(_context5) {
|
|
4575
|
+
while (1) switch (_context5.prev = _context5.next) {
|
|
4576
|
+
case 0:
|
|
4577
|
+
_context5.next = 2;
|
|
4578
|
+
return _ensureFreshToken();
|
|
4579
|
+
case 2:
|
|
4580
|
+
if (!(!_token || _buffer.length === 0)) {
|
|
4581
|
+
_context5.next = 4;
|
|
4582
|
+
break;
|
|
4583
|
+
}
|
|
4584
|
+
return _context5.abrupt("return");
|
|
4585
|
+
case 4:
|
|
4586
|
+
events = _buffer.splice(0, _buffer.length);
|
|
4587
|
+
body = JSON.stringify({
|
|
4588
|
+
warehouse_id: _warehouseId,
|
|
4589
|
+
organization_id: _organizationId,
|
|
4590
|
+
events: events
|
|
4591
|
+
});
|
|
4592
|
+
headers = {
|
|
4593
|
+
'Content-Type': 'application/json',
|
|
4594
|
+
Authorization: "Bearer ".concat(_token)
|
|
4595
|
+
};
|
|
4596
|
+
if (navigator.sendBeacon) {
|
|
4597
|
+
blob = new Blob([body], {
|
|
4598
|
+
type: 'application/json'
|
|
4599
|
+
});
|
|
4600
|
+
navigator.sendBeacon("".concat(INGEST_BASE, "/api/ingest/events"), blob);
|
|
4601
|
+
} else {
|
|
4602
|
+
fetch("".concat(INGEST_BASE, "/api/ingest/events"), {
|
|
4603
|
+
method: 'POST',
|
|
4604
|
+
headers: headers,
|
|
4605
|
+
body: body,
|
|
4606
|
+
keepalive: true
|
|
4607
|
+
})["catch"](function (err) {
|
|
4608
|
+
return console.error('[CoreOutline] Flush error:', err);
|
|
4609
|
+
});
|
|
4610
|
+
}
|
|
4611
|
+
case 8:
|
|
4612
|
+
case "end":
|
|
4613
|
+
return _context5.stop();
|
|
4614
|
+
}
|
|
4615
|
+
}, _callee5);
|
|
4616
|
+
}));
|
|
4617
|
+
return _flush.apply(this, arguments);
|
|
4512
4618
|
}
|
|
4513
|
-
function sendRrwebBatch(
|
|
4619
|
+
function sendRrwebBatch(_x7, _x8) {
|
|
4514
4620
|
return _sendRrwebBatch.apply(this, arguments);
|
|
4515
4621
|
}
|
|
4516
4622
|
function _sendRrwebBatch() {
|
|
4517
|
-
_sendRrwebBatch = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function
|
|
4518
|
-
return _regeneratorRuntime().wrap(function
|
|
4519
|
-
while (1) switch (
|
|
4623
|
+
_sendRrwebBatch = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee6(chunks, sessionId) {
|
|
4624
|
+
return _regeneratorRuntime().wrap(function _callee6$(_context6) {
|
|
4625
|
+
while (1) switch (_context6.prev = _context6.next) {
|
|
4520
4626
|
case 0:
|
|
4627
|
+
_context6.next = 2;
|
|
4628
|
+
return _ensureFreshToken();
|
|
4629
|
+
case 2:
|
|
4521
4630
|
if (!(!_token || !chunks || chunks.length === 0)) {
|
|
4522
|
-
|
|
4631
|
+
_context6.next = 4;
|
|
4523
4632
|
break;
|
|
4524
4633
|
}
|
|
4525
|
-
return
|
|
4526
|
-
case
|
|
4527
|
-
|
|
4528
|
-
|
|
4634
|
+
return _context6.abrupt("return");
|
|
4635
|
+
case 4:
|
|
4636
|
+
_context6.prev = 4;
|
|
4637
|
+
_context6.next = 7;
|
|
4529
4638
|
return fetch("".concat(INGEST_BASE, "/api/ingest/rrweb"), {
|
|
4530
4639
|
method: 'POST',
|
|
4531
4640
|
headers: {
|
|
@@ -4541,18 +4650,18 @@ function _sendRrwebBatch() {
|
|
|
4541
4650
|
}),
|
|
4542
4651
|
keepalive: true
|
|
4543
4652
|
});
|
|
4544
|
-
case 5:
|
|
4545
|
-
_context2.next = 10;
|
|
4546
|
-
break;
|
|
4547
4653
|
case 7:
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4654
|
+
_context6.next = 12;
|
|
4655
|
+
break;
|
|
4656
|
+
case 9:
|
|
4657
|
+
_context6.prev = 9;
|
|
4658
|
+
_context6.t0 = _context6["catch"](4);
|
|
4659
|
+
console.error('[CoreOutline] rrweb upload error:', _context6.t0);
|
|
4660
|
+
case 12:
|
|
4552
4661
|
case "end":
|
|
4553
|
-
return
|
|
4662
|
+
return _context6.stop();
|
|
4554
4663
|
}
|
|
4555
|
-
},
|
|
4664
|
+
}, _callee6, null, [[4, 9]]);
|
|
4556
4665
|
}));
|
|
4557
4666
|
return _sendRrwebBatch.apply(this, arguments);
|
|
4558
4667
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
@@ -4419,25 +4420,57 @@ function getPagePath() {
|
|
|
4419
4420
|
|
|
4420
4421
|
var INGEST_BASE = 'https://atlas-orchestrator.atlas.coreoutline.com';
|
|
4421
4422
|
var FLUSH_INTERVAL_MS = 5000;
|
|
4423
|
+
// Refresh the token this many milliseconds before it actually expires.
|
|
4424
|
+
var TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
4425
|
+
|
|
4422
4426
|
var _token = null;
|
|
4423
4427
|
var _warehouseId = null;
|
|
4424
4428
|
var _organizationId = null;
|
|
4425
4429
|
var _dataSourceId = null;
|
|
4430
|
+
var _dataSourceSecret = null;
|
|
4426
4431
|
var _buffer = [];
|
|
4427
4432
|
var _flushInterval = null;
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4433
|
+
var _refreshTimeout = null;
|
|
4434
|
+
function _scheduleTokenRefresh(expiresInSeconds) {
|
|
4435
|
+
if (_refreshTimeout) {
|
|
4436
|
+
clearTimeout(_refreshTimeout);
|
|
4437
|
+
_refreshTimeout = null;
|
|
4438
|
+
}
|
|
4439
|
+
var refreshInMs = expiresInSeconds * 1000 - TOKEN_REFRESH_BUFFER_MS;
|
|
4440
|
+
if (refreshInMs <= 0) {
|
|
4441
|
+
// Token already near expiry — refresh immediately on next action.
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
_refreshTimeout = setTimeout(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
|
|
4445
|
+
var ok;
|
|
4434
4446
|
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
4435
4447
|
while (1) switch (_context.prev = _context.next) {
|
|
4436
4448
|
case 0:
|
|
4437
|
-
|
|
4438
|
-
_dataSourceId
|
|
4439
|
-
|
|
4440
|
-
|
|
4449
|
+
_context.next = 2;
|
|
4450
|
+
return _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
4451
|
+
case 2:
|
|
4452
|
+
ok = _context.sent;
|
|
4453
|
+
if (!ok) {
|
|
4454
|
+
console.warn('[CoreOutline] Proactive token refresh failed; will retry on next flush.');
|
|
4455
|
+
}
|
|
4456
|
+
case 4:
|
|
4457
|
+
case "end":
|
|
4458
|
+
return _context.stop();
|
|
4459
|
+
}
|
|
4460
|
+
}, _callee);
|
|
4461
|
+
})), refreshInMs);
|
|
4462
|
+
}
|
|
4463
|
+
function _doAuthorize(_x, _x2, _x3) {
|
|
4464
|
+
return _doAuthorize2.apply(this, arguments);
|
|
4465
|
+
}
|
|
4466
|
+
function _doAuthorize2() {
|
|
4467
|
+
_doAuthorize2 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee2(warehouseId, dataSourceId, dataSourceSecret) {
|
|
4468
|
+
var res, data, expiresIn;
|
|
4469
|
+
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
|
|
4470
|
+
while (1) switch (_context2.prev = _context2.next) {
|
|
4471
|
+
case 0:
|
|
4472
|
+
_context2.prev = 0;
|
|
4473
|
+
_context2.next = 3;
|
|
4441
4474
|
return fetch("".concat(INGEST_BASE, "/api/ingest/authorize"), {
|
|
4442
4475
|
method: 'POST',
|
|
4443
4476
|
headers: {
|
|
@@ -4449,35 +4482,85 @@ function _initIngest() {
|
|
|
4449
4482
|
data_source_secret: dataSourceSecret
|
|
4450
4483
|
})
|
|
4451
4484
|
});
|
|
4452
|
-
case
|
|
4453
|
-
res =
|
|
4485
|
+
case 3:
|
|
4486
|
+
res = _context2.sent;
|
|
4454
4487
|
if (res.ok) {
|
|
4455
|
-
|
|
4488
|
+
_context2.next = 7;
|
|
4456
4489
|
break;
|
|
4457
4490
|
}
|
|
4458
4491
|
console.error('[CoreOutline] Authorization failed:', res.status);
|
|
4459
|
-
return
|
|
4460
|
-
case
|
|
4461
|
-
|
|
4492
|
+
return _context2.abrupt("return", false);
|
|
4493
|
+
case 7:
|
|
4494
|
+
_context2.next = 9;
|
|
4462
4495
|
return res.json();
|
|
4463
|
-
case
|
|
4464
|
-
data =
|
|
4496
|
+
case 9:
|
|
4497
|
+
data = _context2.sent;
|
|
4465
4498
|
_token = data.access_token;
|
|
4466
4499
|
_organizationId = data.organization_id;
|
|
4467
|
-
|
|
4500
|
+
expiresIn = data.expires_in || 24 * 3600;
|
|
4501
|
+
_scheduleTokenRefresh(expiresIn);
|
|
4502
|
+
return _context2.abrupt("return", true);
|
|
4468
4503
|
case 17:
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
console.error('[CoreOutline] Authorization error:',
|
|
4472
|
-
return
|
|
4504
|
+
_context2.prev = 17;
|
|
4505
|
+
_context2.t0 = _context2["catch"](0);
|
|
4506
|
+
console.error('[CoreOutline] Authorization error:', _context2.t0);
|
|
4507
|
+
return _context2.abrupt("return", false);
|
|
4473
4508
|
case 21:
|
|
4474
4509
|
case "end":
|
|
4475
|
-
return
|
|
4510
|
+
return _context2.stop();
|
|
4511
|
+
}
|
|
4512
|
+
}, _callee2, null, [[0, 17]]);
|
|
4513
|
+
}));
|
|
4514
|
+
return _doAuthorize2.apply(this, arguments);
|
|
4515
|
+
}
|
|
4516
|
+
function initIngest(_x4, _x5, _x6) {
|
|
4517
|
+
return _initIngest.apply(this, arguments);
|
|
4518
|
+
}
|
|
4519
|
+
function _initIngest() {
|
|
4520
|
+
_initIngest = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee3(warehouseId, dataSourceId, dataSourceSecret) {
|
|
4521
|
+
return _regeneratorRuntime().wrap(function _callee3$(_context3) {
|
|
4522
|
+
while (1) switch (_context3.prev = _context3.next) {
|
|
4523
|
+
case 0:
|
|
4524
|
+
_warehouseId = warehouseId;
|
|
4525
|
+
_dataSourceId = dataSourceId;
|
|
4526
|
+
_dataSourceSecret = dataSourceSecret;
|
|
4527
|
+
return _context3.abrupt("return", _doAuthorize(warehouseId, dataSourceId, dataSourceSecret));
|
|
4528
|
+
case 4:
|
|
4529
|
+
case "end":
|
|
4530
|
+
return _context3.stop();
|
|
4476
4531
|
}
|
|
4477
|
-
},
|
|
4532
|
+
}, _callee3);
|
|
4478
4533
|
}));
|
|
4479
4534
|
return _initIngest.apply(this, arguments);
|
|
4480
4535
|
}
|
|
4536
|
+
function _ensureFreshToken() {
|
|
4537
|
+
return _ensureFreshToken2.apply(this, arguments);
|
|
4538
|
+
}
|
|
4539
|
+
function _ensureFreshToken2() {
|
|
4540
|
+
_ensureFreshToken2 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee4() {
|
|
4541
|
+
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
|
4542
|
+
while (1) switch (_context4.prev = _context4.next) {
|
|
4543
|
+
case 0:
|
|
4544
|
+
if (!_token) {
|
|
4545
|
+
_context4.next = 2;
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
return _context4.abrupt("return");
|
|
4549
|
+
case 2:
|
|
4550
|
+
if (!(_warehouseId && _dataSourceId && _dataSourceSecret)) {
|
|
4551
|
+
_context4.next = 5;
|
|
4552
|
+
break;
|
|
4553
|
+
}
|
|
4554
|
+
_context4.next = 5;
|
|
4555
|
+
return _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
4556
|
+
case 5:
|
|
4557
|
+
case "end":
|
|
4558
|
+
return _context4.stop();
|
|
4559
|
+
}
|
|
4560
|
+
}, _callee4);
|
|
4561
|
+
}));
|
|
4562
|
+
return _ensureFreshToken2.apply(this, arguments);
|
|
4563
|
+
}
|
|
4481
4564
|
function trackEvent(eventType, payload) {
|
|
4482
4565
|
if (!_token) return;
|
|
4483
4566
|
var event = _objectSpread2({
|
|
@@ -4491,49 +4574,75 @@ function trackEvent(eventType, payload) {
|
|
|
4491
4574
|
}
|
|
4492
4575
|
}
|
|
4493
4576
|
function flush() {
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4577
|
+
return _flush.apply(this, arguments);
|
|
4578
|
+
}
|
|
4579
|
+
function _flush() {
|
|
4580
|
+
_flush = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee5() {
|
|
4581
|
+
var events, body, headers, blob;
|
|
4582
|
+
return _regeneratorRuntime().wrap(function _callee5$(_context5) {
|
|
4583
|
+
while (1) switch (_context5.prev = _context5.next) {
|
|
4584
|
+
case 0:
|
|
4585
|
+
_context5.next = 2;
|
|
4586
|
+
return _ensureFreshToken();
|
|
4587
|
+
case 2:
|
|
4588
|
+
if (!(!_token || _buffer.length === 0)) {
|
|
4589
|
+
_context5.next = 4;
|
|
4590
|
+
break;
|
|
4591
|
+
}
|
|
4592
|
+
return _context5.abrupt("return");
|
|
4593
|
+
case 4:
|
|
4594
|
+
events = _buffer.splice(0, _buffer.length);
|
|
4595
|
+
body = JSON.stringify({
|
|
4596
|
+
warehouse_id: _warehouseId,
|
|
4597
|
+
organization_id: _organizationId,
|
|
4598
|
+
events: events
|
|
4599
|
+
});
|
|
4600
|
+
headers = {
|
|
4601
|
+
'Content-Type': 'application/json',
|
|
4602
|
+
Authorization: "Bearer ".concat(_token)
|
|
4603
|
+
};
|
|
4604
|
+
if (navigator.sendBeacon) {
|
|
4605
|
+
blob = new Blob([body], {
|
|
4606
|
+
type: 'application/json'
|
|
4607
|
+
});
|
|
4608
|
+
navigator.sendBeacon("".concat(INGEST_BASE, "/api/ingest/events"), blob);
|
|
4609
|
+
} else {
|
|
4610
|
+
fetch("".concat(INGEST_BASE, "/api/ingest/events"), {
|
|
4611
|
+
method: 'POST',
|
|
4612
|
+
headers: headers,
|
|
4613
|
+
body: body,
|
|
4614
|
+
keepalive: true
|
|
4615
|
+
})["catch"](function (err) {
|
|
4616
|
+
return console.error('[CoreOutline] Flush error:', err);
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
case 8:
|
|
4620
|
+
case "end":
|
|
4621
|
+
return _context5.stop();
|
|
4622
|
+
}
|
|
4623
|
+
}, _callee5);
|
|
4624
|
+
}));
|
|
4625
|
+
return _flush.apply(this, arguments);
|
|
4520
4626
|
}
|
|
4521
|
-
function sendRrwebBatch(
|
|
4627
|
+
function sendRrwebBatch(_x7, _x8) {
|
|
4522
4628
|
return _sendRrwebBatch.apply(this, arguments);
|
|
4523
4629
|
}
|
|
4524
4630
|
function _sendRrwebBatch() {
|
|
4525
|
-
_sendRrwebBatch = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function
|
|
4526
|
-
return _regeneratorRuntime().wrap(function
|
|
4527
|
-
while (1) switch (
|
|
4631
|
+
_sendRrwebBatch = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee6(chunks, sessionId) {
|
|
4632
|
+
return _regeneratorRuntime().wrap(function _callee6$(_context6) {
|
|
4633
|
+
while (1) switch (_context6.prev = _context6.next) {
|
|
4528
4634
|
case 0:
|
|
4635
|
+
_context6.next = 2;
|
|
4636
|
+
return _ensureFreshToken();
|
|
4637
|
+
case 2:
|
|
4529
4638
|
if (!(!_token || !chunks || chunks.length === 0)) {
|
|
4530
|
-
|
|
4639
|
+
_context6.next = 4;
|
|
4531
4640
|
break;
|
|
4532
4641
|
}
|
|
4533
|
-
return
|
|
4534
|
-
case
|
|
4535
|
-
|
|
4536
|
-
|
|
4642
|
+
return _context6.abrupt("return");
|
|
4643
|
+
case 4:
|
|
4644
|
+
_context6.prev = 4;
|
|
4645
|
+
_context6.next = 7;
|
|
4537
4646
|
return fetch("".concat(INGEST_BASE, "/api/ingest/rrweb"), {
|
|
4538
4647
|
method: 'POST',
|
|
4539
4648
|
headers: {
|
|
@@ -4549,18 +4658,18 @@ function _sendRrwebBatch() {
|
|
|
4549
4658
|
}),
|
|
4550
4659
|
keepalive: true
|
|
4551
4660
|
});
|
|
4552
|
-
case 5:
|
|
4553
|
-
_context2.next = 10;
|
|
4554
|
-
break;
|
|
4555
4661
|
case 7:
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4662
|
+
_context6.next = 12;
|
|
4663
|
+
break;
|
|
4664
|
+
case 9:
|
|
4665
|
+
_context6.prev = 9;
|
|
4666
|
+
_context6.t0 = _context6["catch"](4);
|
|
4667
|
+
console.error('[CoreOutline] rrweb upload error:', _context6.t0);
|
|
4668
|
+
case 12:
|
|
4560
4669
|
case "end":
|
|
4561
|
-
return
|
|
4670
|
+
return _context6.stop();
|
|
4562
4671
|
}
|
|
4563
|
-
},
|
|
4672
|
+
}, _callee6, null, [[4, 9]]);
|
|
4564
4673
|
}));
|
|
4565
4674
|
return _sendRrwebBatch.apply(this, arguments);
|
|
4566
4675
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-outline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.25",
|
|
4
4
|
"description": "A React component for Core&Outline",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.es.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"scripts": {
|
|
8
11
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
12
|
"storybook": "storybook dev -p 6006",
|
package/.babelrc
DELETED
package/.eslintignore
DELETED
package/.eslintrc.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"browser": true,
|
|
4
|
-
"es2021": true
|
|
5
|
-
},
|
|
6
|
-
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
|
|
7
|
-
"parserOptions": {
|
|
8
|
-
"ecmaFeatures": {
|
|
9
|
-
"jsx": true
|
|
10
|
-
},
|
|
11
|
-
"ecmaVersion": 12,
|
|
12
|
-
"sourceType": "module"
|
|
13
|
-
},
|
|
14
|
-
"plugins": ["react"],
|
|
15
|
-
"rules": {
|
|
16
|
-
"react/react-in-jsx-scope": "off"
|
|
17
|
-
},
|
|
18
|
-
"settings": {
|
|
19
|
-
"react": {
|
|
20
|
-
"version": "detect"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
package/.prettierignore
DELETED
package/.prettierrc
DELETED
package/.storybook/main.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
|
|
2
|
-
const config = {
|
|
3
|
-
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
4
|
-
addons: [
|
|
5
|
-
'@storybook/addon-webpack5-compiler-swc',
|
|
6
|
-
'@storybook/addon-onboarding',
|
|
7
|
-
'@storybook/addon-essentials',
|
|
8
|
-
'@chromatic-com/storybook',
|
|
9
|
-
'@storybook/addon-interactions',
|
|
10
|
-
],
|
|
11
|
-
framework: {
|
|
12
|
-
name: '@storybook/react-webpack5',
|
|
13
|
-
options: {},
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
export default config;
|
package/.storybook/preview.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Tracking Pipeline — react-component Changes
|
|
2
|
-
Date: 2026-06-27
|
|
3
|
-
|
|
4
|
-
## Summary
|
|
5
|
-
Replaced Socket.IO with direct HTTP calls to the Hermes ingest API. Added warehouse_id-based routing, improved event schema, and removed dev artifacts.
|
|
6
|
-
|
|
7
|
-
## Breaking Changes
|
|
8
|
-
- `<CoreOutline>` now requires a `warehouse_id` prop (in addition to existing `data_source_id` + `data_source_secret`)
|
|
9
|
-
- The `data_source_secret` is now validated against Hermes's Vault store; the old `api.coreoutline.com/data-source/authorize` endpoint is no longer used
|
|
10
|
-
|
|
11
|
-
## New Files
|
|
12
|
-
- `src/components/CoreOutline/helpers.js` — browser utilities: `getAnonymousId`, `getSessionId`, `getUtmParams`, `detectDeviceType`, `detectOS`, `getBrowserName`, `getPagePath`
|
|
13
|
-
- `src/components/CoreOutline/ingest.js` — HTTP ingest client: `initIngest`, `trackEvent`, `flush`, `sendRrwebBatch`, `stopFlushInterval`
|
|
14
|
-
|
|
15
|
-
## Modified Files
|
|
16
|
-
|
|
17
|
-
### `src/components/CoreOutline/CoreOutline.js`
|
|
18
|
-
- Added `warehouse_id` prop
|
|
19
|
-
- Replaced all `socket.send()` calls with `trackEvent()` from `ingest.js`
|
|
20
|
-
- Events now include: UTM params, device type, OS, page_url, page_path, anonymous_id (persistent cross-session), session_id (per-tab via sessionStorage)
|
|
21
|
-
- Removed `getLocation()` / geolocation (replaced by server-side IP geolocation)
|
|
22
|
-
- Removed dev-only `<button>Save Events Locally</button>` from rendered output
|
|
23
|
-
- `flag_item_clicked(item_id)` and `flag_item_purchased(item_id, price)` rewritten to use `trackEvent()`; `flag_item_purchased` now accepts an optional `price` parameter mapped to `value_num`
|
|
24
|
-
- Events auto-flush every 5 seconds and on page unload/visibility change
|
|
25
|
-
- rrweb recordings uploaded via `sendRrwebBatch` at session end
|
|
26
|
-
|
|
27
|
-
### `package.json`
|
|
28
|
-
- Removed: `socket.io-client` (~100KB bundle savings)
|
|
29
|
-
- Added: `uuid` as explicit dependency (was transitive via rrweb)
|
|
30
|
-
|
|
31
|
-
## Event Types Emitted
|
|
32
|
-
| event_type | event_category | Destination table |
|
|
33
|
-
|---|---|---|
|
|
34
|
-
| `session_start` | `session` | `fact_session` |
|
|
35
|
-
| `session_end` | `session` | `fact_session` (update via ReplacingMergeTree) |
|
|
36
|
-
| `pageview` | `navigation` | `fact_pageview` + `fact_event` |
|
|
37
|
-
| `item_clicked` | `interaction` | `fact_event` |
|
|
38
|
-
| `product_clicked` | `commerce` | `fact_event` |
|
|
39
|
-
| `product_purchased` | `commerce` | `fact_event` (value_num = price) |
|
|
40
|
-
|
|
41
|
-
## Data Flow
|
|
42
|
-
react-component → `POST https://api.coreoutline.com/api/ingest/authorize` (get JWT)
|
|
43
|
-
→ `POST https://api.coreoutline.com/api/ingest/events` (batch, every 5s + on unload)
|
|
44
|
-
→ `POST https://api.coreoutline.com/api/ingest/rrweb` (session end)
|
package/rollup.config.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import babel from 'rollup-plugin-babel';
|
|
2
|
-
import resolve from '@rollup/plugin-node-resolve';
|
|
3
|
-
import external from 'rollup-plugin-peer-deps-external';
|
|
4
|
-
import { terser } from 'rollup-plugin-terser';
|
|
5
|
-
import postcss from 'rollup-plugin-postcss';
|
|
6
|
-
|
|
7
|
-
export default [
|
|
8
|
-
{
|
|
9
|
-
input: './src/components/CoreOutline/CoreOutline.js',
|
|
10
|
-
output: [
|
|
11
|
-
{
|
|
12
|
-
file: 'dist/index.js',
|
|
13
|
-
format: 'cjs',
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
file: 'dist/index.es.js',
|
|
17
|
-
format: 'es',
|
|
18
|
-
exports: 'named',
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
plugins: [
|
|
22
|
-
postcss({
|
|
23
|
-
plugins: [],
|
|
24
|
-
minimize: true,
|
|
25
|
-
}),
|
|
26
|
-
babel({
|
|
27
|
-
exclude: 'node_modules/**',
|
|
28
|
-
presets: ['@babel/preset-react'],
|
|
29
|
-
}),
|
|
30
|
-
external(),
|
|
31
|
-
resolve(),
|
|
32
|
-
// terser(),
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
];
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { record } from 'rrweb';
|
|
3
|
-
import {
|
|
4
|
-
EventType,
|
|
5
|
-
IncrementalSource,
|
|
6
|
-
MouseInteractions,
|
|
7
|
-
} from 'rrweb';
|
|
8
|
-
import {
|
|
9
|
-
getAnonymousId,
|
|
10
|
-
getSessionId,
|
|
11
|
-
getUtmParams,
|
|
12
|
-
detectDeviceType,
|
|
13
|
-
detectOS,
|
|
14
|
-
getBrowserName,
|
|
15
|
-
getPagePath,
|
|
16
|
-
} from './helpers';
|
|
17
|
-
import {
|
|
18
|
-
initIngest,
|
|
19
|
-
trackEvent,
|
|
20
|
-
flush,
|
|
21
|
-
sendRrwebBatch,
|
|
22
|
-
stopFlushInterval,
|
|
23
|
-
} from './ingest';
|
|
24
|
-
|
|
25
|
-
const CoreOutline = ({ children, data_source_id, data_source_secret, warehouse_id }) => {
|
|
26
|
-
const [replayEvents, setReplayEvents] = useState([]);
|
|
27
|
-
const recorderActive = useRef(false);
|
|
28
|
-
const [currentPage, setCurrentPage] = useState(window.location.href);
|
|
29
|
-
const pageviewCountRef = useRef(0);
|
|
30
|
-
const eventCountRef = useRef(0);
|
|
31
|
-
const sessionStartRef = useRef(Date.now());
|
|
32
|
-
|
|
33
|
-
const sessionId = getSessionId();
|
|
34
|
-
const anonymousId = getAnonymousId();
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
const utmParams = getUtmParams();
|
|
38
|
-
const deviceType = detectDeviceType();
|
|
39
|
-
const os = detectOS();
|
|
40
|
-
const browser = getBrowserName();
|
|
41
|
-
const referrer = document.referrer || 'direct';
|
|
42
|
-
|
|
43
|
-
const basePayload = {
|
|
44
|
-
session_id: sessionId,
|
|
45
|
-
anonymous_id: anonymousId,
|
|
46
|
-
account_id: data_source_id,
|
|
47
|
-
platform: 'web',
|
|
48
|
-
device_type: deviceType,
|
|
49
|
-
os,
|
|
50
|
-
browser,
|
|
51
|
-
referrer,
|
|
52
|
-
page_url: window.location.href,
|
|
53
|
-
page_path: getPagePath(),
|
|
54
|
-
...utmParams,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
let initialized = false;
|
|
58
|
-
|
|
59
|
-
const init = async () => {
|
|
60
|
-
initialized = await initIngest(warehouse_id, data_source_id, data_source_secret);
|
|
61
|
-
if (!initialized) return;
|
|
62
|
-
|
|
63
|
-
pageviewCountRef.current += 1;
|
|
64
|
-
trackEvent('pageview', { ...basePayload, event_name: 'pageview', event_category: 'navigation' });
|
|
65
|
-
trackEvent('session_start', {
|
|
66
|
-
...basePayload,
|
|
67
|
-
event_name: 'session_start',
|
|
68
|
-
event_category: 'session',
|
|
69
|
-
session_start_ts: new Date().toISOString(),
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
init();
|
|
74
|
-
|
|
75
|
-
const handleSessionEnd = () => {
|
|
76
|
-
if (!initialized) return;
|
|
77
|
-
const durationMs = Date.now() - sessionStartRef.current;
|
|
78
|
-
trackEvent('session_end', {
|
|
79
|
-
...basePayload,
|
|
80
|
-
event_name: 'session_end',
|
|
81
|
-
event_category: 'session',
|
|
82
|
-
session_end_ts: new Date().toISOString(),
|
|
83
|
-
pageviews: pageviewCountRef.current,
|
|
84
|
-
events_count: eventCountRef.current,
|
|
85
|
-
duration_ms: durationMs,
|
|
86
|
-
});
|
|
87
|
-
flush();
|
|
88
|
-
stopFlushInterval();
|
|
89
|
-
sendRrwebBatch(replayEvents, sessionId);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const handleVisibilityChange = () => {
|
|
93
|
-
if (document.visibilityState === 'hidden') handleSessionEnd();
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
window.addEventListener('beforeunload', handleSessionEnd);
|
|
97
|
-
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
98
|
-
|
|
99
|
-
return () => {
|
|
100
|
-
handleSessionEnd();
|
|
101
|
-
window.removeEventListener('beforeunload', handleSessionEnd);
|
|
102
|
-
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
103
|
-
};
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
const utmParams = getUtmParams();
|
|
108
|
-
|
|
109
|
-
const handleNavigation = () => {
|
|
110
|
-
setCurrentPage(window.location.href);
|
|
111
|
-
pageviewCountRef.current += 1;
|
|
112
|
-
trackEvent('pageview', {
|
|
113
|
-
session_id: sessionId,
|
|
114
|
-
anonymous_id: anonymousId,
|
|
115
|
-
account_id: data_source_id,
|
|
116
|
-
platform: 'web',
|
|
117
|
-
page_url: window.location.href,
|
|
118
|
-
page_path: getPagePath(),
|
|
119
|
-
referrer: document.referrer || 'direct',
|
|
120
|
-
event_name: 'pageview',
|
|
121
|
-
event_category: 'navigation',
|
|
122
|
-
...utmParams,
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
window.addEventListener('popstate', handleNavigation);
|
|
127
|
-
const originalPushState = window.history.pushState;
|
|
128
|
-
window.history.pushState = function (...args) {
|
|
129
|
-
originalPushState.apply(this, args);
|
|
130
|
-
handleNavigation();
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
return () => {
|
|
134
|
-
window.removeEventListener('popstate', handleNavigation);
|
|
135
|
-
window.history.pushState = originalPushState;
|
|
136
|
-
};
|
|
137
|
-
}, [currentPage]);
|
|
138
|
-
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
let stopFn;
|
|
141
|
-
if (!recorderActive.current) {
|
|
142
|
-
stopFn = record({
|
|
143
|
-
emit(event) {
|
|
144
|
-
setReplayEvents((prev) => [...prev, event]);
|
|
145
|
-
},
|
|
146
|
-
maskAllInputs: false,
|
|
147
|
-
maskTextSelector: null,
|
|
148
|
-
});
|
|
149
|
-
recorderActive.current = true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return () => {
|
|
153
|
-
if (stopFn) {
|
|
154
|
-
stopFn();
|
|
155
|
-
recorderActive.current = false;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}, []);
|
|
159
|
-
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
const handleClick = (event) => {
|
|
162
|
-
const clickedElement = document.elementFromPoint(event.clientX, event.clientY);
|
|
163
|
-
const metadata = clickedElement
|
|
164
|
-
? {
|
|
165
|
-
label: clickedElement.getAttribute('data-label') || clickedElement.innerText || null,
|
|
166
|
-
value: clickedElement.getAttribute('value') || clickedElement.innerText || null,
|
|
167
|
-
id: clickedElement.getAttribute('id') || null,
|
|
168
|
-
name: clickedElement.getAttribute('name') || null,
|
|
169
|
-
class: clickedElement.getAttribute('class') || null,
|
|
170
|
-
tag: clickedElement.tagName,
|
|
171
|
-
inner_text: event.target.innerText || null,
|
|
172
|
-
}
|
|
173
|
-
: {};
|
|
174
|
-
|
|
175
|
-
eventCountRef.current += 1;
|
|
176
|
-
trackEvent('item_clicked', {
|
|
177
|
-
session_id: sessionId,
|
|
178
|
-
anonymous_id: anonymousId,
|
|
179
|
-
account_id: data_source_id,
|
|
180
|
-
platform: 'web',
|
|
181
|
-
page_url: window.location.href,
|
|
182
|
-
page_path: getPagePath(),
|
|
183
|
-
event_name: 'item_clicked',
|
|
184
|
-
event_category: 'interaction',
|
|
185
|
-
properties_json: JSON.stringify(metadata),
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
document.addEventListener('click', handleClick);
|
|
190
|
-
return () => document.removeEventListener('click', handleClick);
|
|
191
|
-
}, []);
|
|
192
|
-
|
|
193
|
-
return <>{children}</>;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
export const flag_item_clicked = (item_id) => {
|
|
197
|
-
const sessionId = sessionStorage.getItem('co_session_id') || '';
|
|
198
|
-
const anonymousId = localStorage.getItem('co_anon_id') || '';
|
|
199
|
-
trackEvent('product_clicked', {
|
|
200
|
-
session_id: sessionId,
|
|
201
|
-
anonymous_id: anonymousId,
|
|
202
|
-
event_name: 'product_clicked',
|
|
203
|
-
event_category: 'commerce',
|
|
204
|
-
feature_key: item_id,
|
|
205
|
-
});
|
|
206
|
-
return { status: 'success', message: 'Item click flagged successfully.' };
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export const flag_item_purchased = (item_id, price = null) => {
|
|
210
|
-
const sessionId = sessionStorage.getItem('co_session_id') || '';
|
|
211
|
-
const anonymousId = localStorage.getItem('co_anon_id') || '';
|
|
212
|
-
trackEvent('product_purchased', {
|
|
213
|
-
session_id: sessionId,
|
|
214
|
-
anonymous_id: anonymousId,
|
|
215
|
-
event_name: 'product_purchased',
|
|
216
|
-
event_category: 'commerce',
|
|
217
|
-
feature_key: item_id,
|
|
218
|
-
value_num: price != null ? Number(price) : null,
|
|
219
|
-
});
|
|
220
|
-
return { status: 'success', message: 'Item purchase flagged successfully.' };
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
export default CoreOutline;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
import { record } from 'rrweb';
|
|
3
|
-
|
|
4
|
-
const useRRWebRecorder = () => {
|
|
5
|
-
const eventsRef = useRef([]);
|
|
6
|
-
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
const stopRecording = record({
|
|
9
|
-
emit(event) {
|
|
10
|
-
if (event) {
|
|
11
|
-
eventsRef.current.push(event);
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
return () => {
|
|
17
|
-
if (stopRecording) {
|
|
18
|
-
stopRecording();
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
return eventsRef;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export default useRRWebRecorder;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
|
|
3
|
-
const ANON_ID_KEY = 'co_anon_id';
|
|
4
|
-
const SESSION_ID_KEY = 'co_session_id';
|
|
5
|
-
|
|
6
|
-
export function getAnonymousId() {
|
|
7
|
-
let id = localStorage.getItem(ANON_ID_KEY);
|
|
8
|
-
if (!id) {
|
|
9
|
-
id = uuidv4();
|
|
10
|
-
localStorage.setItem(ANON_ID_KEY, id);
|
|
11
|
-
}
|
|
12
|
-
return id;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function getSessionId() {
|
|
16
|
-
let id = sessionStorage.getItem(SESSION_ID_KEY);
|
|
17
|
-
if (!id) {
|
|
18
|
-
id = uuidv4();
|
|
19
|
-
sessionStorage.setItem(SESSION_ID_KEY, id);
|
|
20
|
-
}
|
|
21
|
-
return id;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getUtmParams() {
|
|
25
|
-
const params = new URLSearchParams(window.location.search);
|
|
26
|
-
return {
|
|
27
|
-
utm_source: params.get('utm_source') || undefined,
|
|
28
|
-
utm_medium: params.get('utm_medium') || undefined,
|
|
29
|
-
utm_campaign: params.get('utm_campaign') || undefined,
|
|
30
|
-
utm_term: params.get('utm_term') || undefined,
|
|
31
|
-
utm_content: params.get('utm_content') || undefined,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function detectDeviceType() {
|
|
36
|
-
const ua = navigator.userAgent;
|
|
37
|
-
if (/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua)) {
|
|
38
|
-
if (/iPad|Tablet/i.test(ua)) return 'tablet';
|
|
39
|
-
return 'mobile';
|
|
40
|
-
}
|
|
41
|
-
return 'desktop';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function detectOS() {
|
|
45
|
-
const ua = navigator.userAgent;
|
|
46
|
-
if (/iPhone|iPad|iPod/i.test(ua)) return 'iOS';
|
|
47
|
-
if (/Android/i.test(ua)) return 'Android';
|
|
48
|
-
if (/Win/i.test(ua)) return 'Windows';
|
|
49
|
-
if (/Mac/i.test(ua)) return 'macOS';
|
|
50
|
-
if (/Linux/i.test(ua)) return 'Linux';
|
|
51
|
-
return 'Other';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function getBrowserName() {
|
|
55
|
-
const ua = navigator.userAgent;
|
|
56
|
-
if (ua.indexOf('Firefox') > -1) return 'Firefox';
|
|
57
|
-
if (ua.indexOf('Opera') > -1 || ua.indexOf('OPR') > -1) return 'Opera';
|
|
58
|
-
if (ua.indexOf('Chrome') > -1) return 'Chrome';
|
|
59
|
-
if (ua.indexOf('Safari') > -1) return 'Safari';
|
|
60
|
-
if (ua.indexOf('MSIE') > -1 || ua.indexOf('Trident/') > -1) return 'IE';
|
|
61
|
-
return 'Unknown';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getPagePath() {
|
|
65
|
-
return window.location.pathname;
|
|
66
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './CoreOutline';
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
|
|
3
|
-
const INGEST_BASE = 'https://atlas-orchestrator.atlas.coreoutline.com';
|
|
4
|
-
const FLUSH_INTERVAL_MS = 5000;
|
|
5
|
-
|
|
6
|
-
let _token = null;
|
|
7
|
-
let _warehouseId = null;
|
|
8
|
-
let _organizationId = null;
|
|
9
|
-
let _dataSourceId = null;
|
|
10
|
-
let _buffer = [];
|
|
11
|
-
let _flushInterval = null;
|
|
12
|
-
|
|
13
|
-
export async function initIngest(warehouseId, dataSourceId, dataSourceSecret) {
|
|
14
|
-
_warehouseId = warehouseId;
|
|
15
|
-
_dataSourceId = dataSourceId;
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const res = await fetch(`${INGEST_BASE}/api/ingest/authorize`, {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: { 'Content-Type': 'application/json' },
|
|
21
|
-
body: JSON.stringify({
|
|
22
|
-
warehouse_id: warehouseId,
|
|
23
|
-
data_source_id: dataSourceId,
|
|
24
|
-
data_source_secret: dataSourceSecret,
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
console.error('[CoreOutline] Authorization failed:', res.status);
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const data = await res.json();
|
|
34
|
-
_token = data.access_token;
|
|
35
|
-
_organizationId = data.organization_id;
|
|
36
|
-
return true;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
console.error('[CoreOutline] Authorization error:', err);
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function trackEvent(eventType, payload) {
|
|
44
|
-
if (!_token) return;
|
|
45
|
-
|
|
46
|
-
const event = {
|
|
47
|
-
event_id: uuidv4(),
|
|
48
|
-
event_type: eventType,
|
|
49
|
-
event_ts: new Date().toISOString(),
|
|
50
|
-
...payload,
|
|
51
|
-
};
|
|
52
|
-
_buffer.push(event);
|
|
53
|
-
|
|
54
|
-
if (!_flushInterval) {
|
|
55
|
-
_flushInterval = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function flush() {
|
|
60
|
-
if (!_token || _buffer.length === 0) return;
|
|
61
|
-
|
|
62
|
-
const events = _buffer.splice(0, _buffer.length);
|
|
63
|
-
const body = JSON.stringify({
|
|
64
|
-
warehouse_id: _warehouseId,
|
|
65
|
-
organization_id: _organizationId,
|
|
66
|
-
events,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const headers = {
|
|
70
|
-
'Content-Type': 'application/json',
|
|
71
|
-
Authorization: `Bearer ${_token}`,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
if (navigator.sendBeacon) {
|
|
75
|
-
const blob = new Blob([body], { type: 'application/json' });
|
|
76
|
-
navigator.sendBeacon(`${INGEST_BASE}/api/ingest/events`, blob);
|
|
77
|
-
} else {
|
|
78
|
-
fetch(`${INGEST_BASE}/api/ingest/events`, {
|
|
79
|
-
method: 'POST',
|
|
80
|
-
headers,
|
|
81
|
-
body,
|
|
82
|
-
keepalive: true,
|
|
83
|
-
}).catch((err) => console.error('[CoreOutline] Flush error:', err));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function sendRrwebBatch(chunks, sessionId) {
|
|
88
|
-
if (!_token || !chunks || chunks.length === 0) return;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
await fetch(`${INGEST_BASE}/api/ingest/rrweb`, {
|
|
92
|
-
method: 'POST',
|
|
93
|
-
headers: {
|
|
94
|
-
'Content-Type': 'application/json',
|
|
95
|
-
Authorization: `Bearer ${_token}`,
|
|
96
|
-
},
|
|
97
|
-
body: JSON.stringify({
|
|
98
|
-
warehouse_id: _warehouseId,
|
|
99
|
-
organization_id: _organizationId,
|
|
100
|
-
session_id: sessionId,
|
|
101
|
-
data_source_id: _dataSourceId,
|
|
102
|
-
chunks,
|
|
103
|
-
}),
|
|
104
|
-
keepalive: true,
|
|
105
|
-
});
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.error('[CoreOutline] rrweb upload error:', err);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function stopFlushInterval() {
|
|
112
|
-
if (_flushInterval) {
|
|
113
|
-
clearInterval(_flushInterval);
|
|
114
|
-
_flushInterval = null;
|
|
115
|
-
}
|
|
116
|
-
}
|
package/src/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './components/CoreOutline';
|