core-outline 1.1.24 → 1.1.26
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 +0 -22
- package/dist/index.es.js +17 -32
- package/dist/index.js +17 -32
- 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 -157
- package/src/components/CoreOutline/socket.js +0 -3
- package/src/index.js +0 -1
package/README.md
CHANGED
|
@@ -12,28 +12,6 @@ npm install core-outline
|
|
|
12
12
|
yarn add core-outline
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
## Prerequisites
|
|
16
|
-
|
|
17
|
-
Before adding the component to your app, create an SDK data source through the Hermes API to obtain your credentials. This requires your Firebase authentication token.
|
|
18
|
-
|
|
19
|
-
```sh
|
|
20
|
-
curl -X POST https://atlas-orchestrator.atlas.coreoutline.com/api/ingest/sdk-sources \
|
|
21
|
-
-H "Authorization: Bearer <your-firebase-token>" \
|
|
22
|
-
-H "Content-Type: application/json" \
|
|
23
|
-
-d '{"name": "My Website"}'
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
The response contains your `data_source_id`, `data_source_secret`, and `warehouse_id`. **The secret is shown once — store it securely.**
|
|
27
|
-
|
|
28
|
-
```json
|
|
29
|
-
{
|
|
30
|
-
"data_source_id": "3f4a1c2d-...",
|
|
31
|
-
"data_source_secret": "aB3xK9mZqR...",
|
|
32
|
-
"warehouse_id": "cad1acc0-...",
|
|
33
|
-
"name": "My Website"
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
15
|
## Usage
|
|
38
16
|
|
|
39
17
|
Wrap your app's root component with `<CoreOutline>`. All tracking is automatic from that point — no additional configuration is needed.
|
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
|
|
|
@@ -4532,19 +4533,13 @@ function _ensureFreshToken2() {
|
|
|
4532
4533
|
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
|
4533
4534
|
while (1) switch (_context4.prev = _context4.next) {
|
|
4534
4535
|
case 0:
|
|
4535
|
-
if (!_token) {
|
|
4536
|
-
_context4.next = 2;
|
|
4537
|
-
break;
|
|
4538
|
-
}
|
|
4539
|
-
return _context4.abrupt("return");
|
|
4540
|
-
case 2:
|
|
4541
4536
|
if (!(_warehouseId && _dataSourceId && _dataSourceSecret)) {
|
|
4542
|
-
_context4.next =
|
|
4537
|
+
_context4.next = 3;
|
|
4543
4538
|
break;
|
|
4544
4539
|
}
|
|
4545
|
-
_context4.next =
|
|
4540
|
+
_context4.next = 3;
|
|
4546
4541
|
return _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
4547
|
-
case
|
|
4542
|
+
case 3:
|
|
4548
4543
|
case "end":
|
|
4549
4544
|
return _context4.stop();
|
|
4550
4545
|
}
|
|
@@ -4553,7 +4548,8 @@ function _ensureFreshToken2() {
|
|
|
4553
4548
|
return _ensureFreshToken2.apply(this, arguments);
|
|
4554
4549
|
}
|
|
4555
4550
|
function trackEvent(eventType, payload) {
|
|
4556
|
-
if (!_token) return;
|
|
4551
|
+
// if (!_token) return;
|
|
4552
|
+
|
|
4557
4553
|
var event = _objectSpread2({
|
|
4558
4554
|
event_id: v4(),
|
|
4559
4555
|
event_type: eventType,
|
|
@@ -4576,12 +4572,7 @@ function _flush() {
|
|
|
4576
4572
|
_context5.next = 2;
|
|
4577
4573
|
return _ensureFreshToken();
|
|
4578
4574
|
case 2:
|
|
4579
|
-
if (!
|
|
4580
|
-
_context5.next = 4;
|
|
4581
|
-
break;
|
|
4582
|
-
}
|
|
4583
|
-
return _context5.abrupt("return");
|
|
4584
|
-
case 4:
|
|
4575
|
+
// if (!_token || _buffer.length === 0) return;
|
|
4585
4576
|
events = _buffer.splice(0, _buffer.length);
|
|
4586
4577
|
body = JSON.stringify({
|
|
4587
4578
|
warehouse_id: _warehouseId,
|
|
@@ -4607,7 +4598,7 @@ function _flush() {
|
|
|
4607
4598
|
return console.error('[CoreOutline] Flush error:', err);
|
|
4608
4599
|
});
|
|
4609
4600
|
}
|
|
4610
|
-
case
|
|
4601
|
+
case 6:
|
|
4611
4602
|
case "end":
|
|
4612
4603
|
return _context5.stop();
|
|
4613
4604
|
}
|
|
@@ -4626,14 +4617,8 @@ function _sendRrwebBatch() {
|
|
|
4626
4617
|
_context6.next = 2;
|
|
4627
4618
|
return _ensureFreshToken();
|
|
4628
4619
|
case 2:
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
break;
|
|
4632
|
-
}
|
|
4633
|
-
return _context6.abrupt("return");
|
|
4634
|
-
case 4:
|
|
4635
|
-
_context6.prev = 4;
|
|
4636
|
-
_context6.next = 7;
|
|
4620
|
+
_context6.prev = 2;
|
|
4621
|
+
_context6.next = 5;
|
|
4637
4622
|
return fetch("".concat(INGEST_BASE, "/api/ingest/rrweb"), {
|
|
4638
4623
|
method: 'POST',
|
|
4639
4624
|
headers: {
|
|
@@ -4649,18 +4634,18 @@ function _sendRrwebBatch() {
|
|
|
4649
4634
|
}),
|
|
4650
4635
|
keepalive: true
|
|
4651
4636
|
});
|
|
4652
|
-
case
|
|
4653
|
-
_context6.next =
|
|
4637
|
+
case 5:
|
|
4638
|
+
_context6.next = 10;
|
|
4654
4639
|
break;
|
|
4655
|
-
case
|
|
4656
|
-
_context6.prev =
|
|
4657
|
-
_context6.t0 = _context6["catch"](
|
|
4640
|
+
case 7:
|
|
4641
|
+
_context6.prev = 7;
|
|
4642
|
+
_context6.t0 = _context6["catch"](2);
|
|
4658
4643
|
console.error('[CoreOutline] rrweb upload error:', _context6.t0);
|
|
4659
|
-
case
|
|
4644
|
+
case 10:
|
|
4660
4645
|
case "end":
|
|
4661
4646
|
return _context6.stop();
|
|
4662
4647
|
}
|
|
4663
|
-
}, _callee6, null, [[
|
|
4648
|
+
}, _callee6, null, [[2, 7]]);
|
|
4664
4649
|
}));
|
|
4665
4650
|
return _sendRrwebBatch.apply(this, arguments);
|
|
4666
4651
|
}
|
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 });
|
|
@@ -4540,19 +4541,13 @@ function _ensureFreshToken2() {
|
|
|
4540
4541
|
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
|
4541
4542
|
while (1) switch (_context4.prev = _context4.next) {
|
|
4542
4543
|
case 0:
|
|
4543
|
-
if (!_token) {
|
|
4544
|
-
_context4.next = 2;
|
|
4545
|
-
break;
|
|
4546
|
-
}
|
|
4547
|
-
return _context4.abrupt("return");
|
|
4548
|
-
case 2:
|
|
4549
4544
|
if (!(_warehouseId && _dataSourceId && _dataSourceSecret)) {
|
|
4550
|
-
_context4.next =
|
|
4545
|
+
_context4.next = 3;
|
|
4551
4546
|
break;
|
|
4552
4547
|
}
|
|
4553
|
-
_context4.next =
|
|
4548
|
+
_context4.next = 3;
|
|
4554
4549
|
return _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
4555
|
-
case
|
|
4550
|
+
case 3:
|
|
4556
4551
|
case "end":
|
|
4557
4552
|
return _context4.stop();
|
|
4558
4553
|
}
|
|
@@ -4561,7 +4556,8 @@ function _ensureFreshToken2() {
|
|
|
4561
4556
|
return _ensureFreshToken2.apply(this, arguments);
|
|
4562
4557
|
}
|
|
4563
4558
|
function trackEvent(eventType, payload) {
|
|
4564
|
-
if (!_token) return;
|
|
4559
|
+
// if (!_token) return;
|
|
4560
|
+
|
|
4565
4561
|
var event = _objectSpread2({
|
|
4566
4562
|
event_id: uuid.v4(),
|
|
4567
4563
|
event_type: eventType,
|
|
@@ -4584,12 +4580,7 @@ function _flush() {
|
|
|
4584
4580
|
_context5.next = 2;
|
|
4585
4581
|
return _ensureFreshToken();
|
|
4586
4582
|
case 2:
|
|
4587
|
-
if (!
|
|
4588
|
-
_context5.next = 4;
|
|
4589
|
-
break;
|
|
4590
|
-
}
|
|
4591
|
-
return _context5.abrupt("return");
|
|
4592
|
-
case 4:
|
|
4583
|
+
// if (!_token || _buffer.length === 0) return;
|
|
4593
4584
|
events = _buffer.splice(0, _buffer.length);
|
|
4594
4585
|
body = JSON.stringify({
|
|
4595
4586
|
warehouse_id: _warehouseId,
|
|
@@ -4615,7 +4606,7 @@ function _flush() {
|
|
|
4615
4606
|
return console.error('[CoreOutline] Flush error:', err);
|
|
4616
4607
|
});
|
|
4617
4608
|
}
|
|
4618
|
-
case
|
|
4609
|
+
case 6:
|
|
4619
4610
|
case "end":
|
|
4620
4611
|
return _context5.stop();
|
|
4621
4612
|
}
|
|
@@ -4634,14 +4625,8 @@ function _sendRrwebBatch() {
|
|
|
4634
4625
|
_context6.next = 2;
|
|
4635
4626
|
return _ensureFreshToken();
|
|
4636
4627
|
case 2:
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
break;
|
|
4640
|
-
}
|
|
4641
|
-
return _context6.abrupt("return");
|
|
4642
|
-
case 4:
|
|
4643
|
-
_context6.prev = 4;
|
|
4644
|
-
_context6.next = 7;
|
|
4628
|
+
_context6.prev = 2;
|
|
4629
|
+
_context6.next = 5;
|
|
4645
4630
|
return fetch("".concat(INGEST_BASE, "/api/ingest/rrweb"), {
|
|
4646
4631
|
method: 'POST',
|
|
4647
4632
|
headers: {
|
|
@@ -4657,18 +4642,18 @@ function _sendRrwebBatch() {
|
|
|
4657
4642
|
}),
|
|
4658
4643
|
keepalive: true
|
|
4659
4644
|
});
|
|
4660
|
-
case
|
|
4661
|
-
_context6.next =
|
|
4645
|
+
case 5:
|
|
4646
|
+
_context6.next = 10;
|
|
4662
4647
|
break;
|
|
4663
|
-
case
|
|
4664
|
-
_context6.prev =
|
|
4665
|
-
_context6.t0 = _context6["catch"](
|
|
4648
|
+
case 7:
|
|
4649
|
+
_context6.prev = 7;
|
|
4650
|
+
_context6.t0 = _context6["catch"](2);
|
|
4666
4651
|
console.error('[CoreOutline] rrweb upload error:', _context6.t0);
|
|
4667
|
-
case
|
|
4652
|
+
case 10:
|
|
4668
4653
|
case "end":
|
|
4669
4654
|
return _context6.stop();
|
|
4670
4655
|
}
|
|
4671
|
-
}, _callee6, null, [[
|
|
4656
|
+
}, _callee6, null, [[2, 7]]);
|
|
4672
4657
|
}));
|
|
4673
4658
|
return _sendRrwebBatch.apply(this, arguments);
|
|
4674
4659
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-outline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.26",
|
|
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,157 +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
|
-
// Refresh the token this many milliseconds before it actually expires.
|
|
6
|
-
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
|
-
|
|
8
|
-
let _token = null;
|
|
9
|
-
let _warehouseId = null;
|
|
10
|
-
let _organizationId = null;
|
|
11
|
-
let _dataSourceId = null;
|
|
12
|
-
let _dataSourceSecret = null;
|
|
13
|
-
let _buffer = [];
|
|
14
|
-
let _flushInterval = null;
|
|
15
|
-
let _refreshTimeout = null;
|
|
16
|
-
|
|
17
|
-
function _scheduleTokenRefresh(expiresInSeconds) {
|
|
18
|
-
if (_refreshTimeout) {
|
|
19
|
-
clearTimeout(_refreshTimeout);
|
|
20
|
-
_refreshTimeout = null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const refreshInMs = expiresInSeconds * 1000 - TOKEN_REFRESH_BUFFER_MS;
|
|
24
|
-
if (refreshInMs <= 0) {
|
|
25
|
-
// Token already near expiry — refresh immediately on next action.
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
_refreshTimeout = setTimeout(async () => {
|
|
30
|
-
const ok = await _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
31
|
-
if (!ok) {
|
|
32
|
-
console.warn('[CoreOutline] Proactive token refresh failed; will retry on next flush.');
|
|
33
|
-
}
|
|
34
|
-
}, refreshInMs);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function _doAuthorize(warehouseId, dataSourceId, dataSourceSecret) {
|
|
38
|
-
try {
|
|
39
|
-
const res = await fetch(`${INGEST_BASE}/api/ingest/authorize`, {
|
|
40
|
-
method: 'POST',
|
|
41
|
-
headers: { 'Content-Type': 'application/json' },
|
|
42
|
-
body: JSON.stringify({
|
|
43
|
-
warehouse_id: warehouseId,
|
|
44
|
-
data_source_id: dataSourceId,
|
|
45
|
-
data_source_secret: dataSourceSecret,
|
|
46
|
-
}),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (!res.ok) {
|
|
50
|
-
console.error('[CoreOutline] Authorization failed:', res.status);
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const data = await res.json();
|
|
55
|
-
_token = data.access_token;
|
|
56
|
-
_organizationId = data.organization_id;
|
|
57
|
-
|
|
58
|
-
const expiresIn = data.expires_in || 24 * 3600;
|
|
59
|
-
_scheduleTokenRefresh(expiresIn);
|
|
60
|
-
return true;
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error('[CoreOutline] Authorization error:', err);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function initIngest(warehouseId, dataSourceId, dataSourceSecret) {
|
|
68
|
-
_warehouseId = warehouseId;
|
|
69
|
-
_dataSourceId = dataSourceId;
|
|
70
|
-
_dataSourceSecret = dataSourceSecret;
|
|
71
|
-
|
|
72
|
-
return _doAuthorize(warehouseId, dataSourceId, dataSourceSecret);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function _ensureFreshToken() {
|
|
76
|
-
if (_token) return;
|
|
77
|
-
if (_warehouseId && _dataSourceId && _dataSourceSecret) {
|
|
78
|
-
await _doAuthorize(_warehouseId, _dataSourceId, _dataSourceSecret);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function trackEvent(eventType, payload) {
|
|
83
|
-
if (!_token) return;
|
|
84
|
-
|
|
85
|
-
const event = {
|
|
86
|
-
event_id: uuidv4(),
|
|
87
|
-
event_type: eventType,
|
|
88
|
-
event_ts: new Date().toISOString(),
|
|
89
|
-
...payload,
|
|
90
|
-
};
|
|
91
|
-
_buffer.push(event);
|
|
92
|
-
|
|
93
|
-
if (!_flushInterval) {
|
|
94
|
-
_flushInterval = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function flush() {
|
|
99
|
-
await _ensureFreshToken();
|
|
100
|
-
if (!_token || _buffer.length === 0) return;
|
|
101
|
-
|
|
102
|
-
const events = _buffer.splice(0, _buffer.length);
|
|
103
|
-
const body = JSON.stringify({
|
|
104
|
-
warehouse_id: _warehouseId,
|
|
105
|
-
organization_id: _organizationId,
|
|
106
|
-
events,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const headers = {
|
|
110
|
-
'Content-Type': 'application/json',
|
|
111
|
-
Authorization: `Bearer ${_token}`,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (navigator.sendBeacon) {
|
|
115
|
-
const blob = new Blob([body], { type: 'application/json' });
|
|
116
|
-
navigator.sendBeacon(`${INGEST_BASE}/api/ingest/events`, blob);
|
|
117
|
-
} else {
|
|
118
|
-
fetch(`${INGEST_BASE}/api/ingest/events`, {
|
|
119
|
-
method: 'POST',
|
|
120
|
-
headers,
|
|
121
|
-
body,
|
|
122
|
-
keepalive: true,
|
|
123
|
-
}).catch((err) => console.error('[CoreOutline] Flush error:', err));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function sendRrwebBatch(chunks, sessionId) {
|
|
128
|
-
await _ensureFreshToken();
|
|
129
|
-
if (!_token || !chunks || chunks.length === 0) return;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
await fetch(`${INGEST_BASE}/api/ingest/rrweb`, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
Authorization: `Bearer ${_token}`,
|
|
137
|
-
},
|
|
138
|
-
body: JSON.stringify({
|
|
139
|
-
warehouse_id: _warehouseId,
|
|
140
|
-
organization_id: _organizationId,
|
|
141
|
-
session_id: sessionId,
|
|
142
|
-
data_source_id: _dataSourceId,
|
|
143
|
-
chunks,
|
|
144
|
-
}),
|
|
145
|
-
keepalive: true,
|
|
146
|
-
});
|
|
147
|
-
} catch (err) {
|
|
148
|
-
console.error('[CoreOutline] rrweb upload error:', err);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function stopFlushInterval() {
|
|
153
|
-
if (_flushInterval) {
|
|
154
|
-
clearInterval(_flushInterval);
|
|
155
|
-
_flushInterval = null;
|
|
156
|
-
}
|
|
157
|
-
}
|
package/src/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './components/CoreOutline';
|