core-outline 1.1.21 → 1.1.23

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.
@@ -0,0 +1,44 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-outline",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
4
4
  "description": "A React component for Core&Outline",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.es.js",
@@ -47,6 +47,6 @@
47
47
  "react-error-boundary": "^5.0.0",
48
48
  "react-tracking": "^9.3.2",
49
49
  "rrweb": "^2.0.0-alpha.4",
50
- "socket.io-client": "^4.8.1"
50
+ "uuid": "^9.0.0"
51
51
  }
52
52
  }
@@ -1,171 +1,125 @@
1
- import React, { use, useEffect, useRef, useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import { record } from 'rrweb';
3
- import { v4 } from 'uuid';
4
- import socket from './socket';
5
-
6
3
  import {
7
4
  EventType,
8
- eventWithTime,
9
5
  IncrementalSource,
10
6
  MouseInteractions,
11
7
  } from 'rrweb';
12
-
13
- const CoreOutline = ({ children, data_source_id, data_source_secret }) => {
14
- const [events, setEvents] = useState([]);
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 }) => {
15
26
  const [replayEvents, setReplayEvents] = useState([]);
16
27
  const recorderActive = useRef(false);
17
28
  const [currentPage, setCurrentPage] = useState(window.location.href);
18
- const [location, setLocation] = useState({ latitude: null, longitude: null });
19
- const [browser, setBrowser] = useState(getBrowserInfo());
20
- const [appToken, setAppToken] = useState('');
21
- const [sessionId, setSessionId] = useState(v4());
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();
22
35
 
23
- async function authorizeApp(data_source_id, data_source_secret) {
24
- const requestOptions = {
25
- method: 'POST',
26
- headers: {
27
- 'Content-Type': 'application/json',
28
- },
29
- body: JSON.stringify({
30
- data_source_id: data_source_id,
31
- data_source_secret: data_source_secret,
32
- }),
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,
33
55
  };
34
- const response = fetch(
35
- `http://api.coreoutline.com/data-source/authorize`,
36
- requestOptions
37
- )
38
- .then((response) => response.json())
39
- .then((data) => {
40
- setAppToken(data.app_token);
41
- return data;
42
- })
43
- .catch((error) => {
44
- console.error('Error:', error);
45
- return error;
46
- });
47
- return response;
48
- }
49
56
 
50
- const offLoadFunctionality = () => {
51
- const formattedEvents = events;
52
- const rawEvents = replayEvents;
53
- const data = JSON.stringify({
54
- data_source_id: data_source_id,
55
- sessionId: sessionId,
56
- formatted_events: formattedEvents,
57
- raw_events: rawEvents,
58
- });
57
+ let initialized = false;
59
58
 
60
- socket.send({
61
- topic: 'session-data',
62
- data_source_id: data_source_id,
63
- session_id: localStorage.getItem('coreOutlineSessionId'),
64
- start_date: new Date().toLocaleString(),
65
- latitude: location.latitude,
66
- longitude: location.longitude,
67
- browser: browser,
68
- event_id: 'SESSION_END',
69
- });
59
+ const init = async () => {
60
+ initialized = await initIngest(warehouse_id, data_source_id, data_source_secret);
61
+ if (!initialized) return;
70
62
 
71
- if (navigator.sendBeacon) {
72
- navigator.sendBeacon(`http://data.coreoutline.com/upload-events`, data);
73
- } else {
74
- fetch(`http://data.coreoutline.com/upload-events`, {
75
- method: 'POST',
76
- body: data,
77
- keepalive: true,
78
- headers: {
79
- 'Content-Type': 'application/json',
80
- },
81
- }).catch((err) => console.error('Upload failed:', err));
82
- }
83
- };
84
- useEffect(() => {}, [events, replayEvents]);
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
+ };
85
72
 
86
- useEffect(async () => {
87
- const handleBeforeUnload = () => {
88
- offLoadFunctionality();
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);
89
90
  };
90
91
 
91
92
  const handleVisibilityChange = () => {
92
- if (document.visibilityState === 'hidden') {
93
- offLoadFunctionality();
94
- }
93
+ if (document.visibilityState === 'hidden') handleSessionEnd();
95
94
  };
96
- window.addEventListener('beforeunload', handleBeforeUnload);
95
+
96
+ window.addEventListener('beforeunload', handleSessionEnd);
97
97
  document.addEventListener('visibilitychange', handleVisibilityChange);
98
- await authorizeApp(data_source_id, data_source_secret);
99
- localStorage.setItem('coreOutlineSessionId', sessionId);
100
- localStorage.setItem('coreOutlineDataSourceIdId', data_source_id);
101
- console.log('Session', sessionId);
102
- socket.send({
103
- topic: 'session-data',
104
- data_source_id: data_source_id,
105
- session_id: localStorage.getItem('coreOutlineSessionId'),
106
- start_date: new Date().toLocaleString(),
107
- latitude: location.latitude,
108
- longitude: location.longitude,
109
- browser: browser,
110
- event_id: 'SESSION_START',
111
- });
112
98
 
113
99
  return () => {
114
- offLoadFunctionality();
115
- window.removeEventListener('beforeunload', handleBeforeUnload);
100
+ handleSessionEnd();
101
+ window.removeEventListener('beforeunload', handleSessionEnd);
116
102
  document.removeEventListener('visibilitychange', handleVisibilityChange);
117
- socket.send({
118
- topic: 'session-data',
119
- data_source_id: data_source_id,
120
- session_id: localStorage.getItem('coreOutlineSessionId'),
121
- start_date: new Date().toLocaleString(),
122
- latitude: location.latitude,
123
- longitude: location.longitude,
124
- browser: browser,
125
- event_id: 'SESSION_END',
126
- });
127
103
  };
128
104
  }, []);
129
105
 
130
106
  useEffect(() => {
131
- const navigationEvent = {
132
- type: 4,
133
- data: {
134
- href: window.location.href,
135
- width: 1536,
136
- height: 730,
137
- },
138
- timestamp: Date.now(),
139
- };
140
- const startTime = performance.now();
141
- const observer = new MutationObserver(() => {
142
- const endTime = performance.now();
143
- const loadTime = endTime - startTime;
144
- navigationEvent.data.loadTime = loadTime;
145
- const hours = Math.floor(loadTime / 3600000);
146
- const minutes = Math.floor((loadTime % 3600000) / 60000);
147
- const seconds = Math.floor((loadTime % 60000) / 1000);
148
- navigationEvent.data.loadTimeFormatted = `${hours} hours ${minutes} minutes ${seconds} seconds`;
149
- observer.disconnect();
150
- });
151
- observer.observe(document, { childList: true, subtree: true });
152
- setEvents((prevEvents) => [...prevEvents, navigationEvent]);
153
- localStorage.setItem(
154
- 'coreoutlineFormattedEvents',
155
- JSON.stringify((prevEvents) => [...prevEvents, navigationEvent])
156
- );
107
+ const utmParams = getUtmParams();
108
+
157
109
  const handleNavigation = () => {
158
110
  setCurrentPage(window.location.href);
159
- socket.send({
160
- topic: 'session-data',
161
- data_source_id: data_source_id,
162
- session_id: localStorage.getItem('coreOutlineSessionId'),
163
- start_date: new Date().toLocaleString(),
164
- latitude: location.latitude,
165
- longitude: location.longitude,
166
- browser: browser,
167
- page_name: window.location.href,
168
- event_id: 'PAGE_NAVIGATION',
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,
169
123
  });
170
124
  };
171
125
 
@@ -182,76 +136,22 @@ const CoreOutline = ({ children, data_source_id, data_source_secret }) => {
182
136
  };
183
137
  }, [currentPage]);
184
138
 
185
- useEffect(() => {
186
- console.log('Location', location);
187
- getLocation();
188
- }, []);
189
-
190
139
  useEffect(() => {
191
140
  let stopFn;
192
141
  if (!recorderActive.current) {
193
142
  stopFn = record({
194
143
  emit(event) {
195
- setReplayEvents((prevEvents) => [...prevEvents, event]);
196
- localStorage.setItem(
197
- 'coreoutlineRawEvents',
198
- JSON.stringify((prevEvents) => [...replayEvents, event])
199
- );
144
+ setReplayEvents((prev) => [...prev, event]);
200
145
  },
201
146
  maskAllInputs: false,
202
147
  maskTextSelector: null,
203
148
  });
204
-
205
149
  recorderActive.current = true;
206
150
  }
207
151
 
208
152
  return () => {
209
153
  if (stopFn) {
210
154
  stopFn();
211
-
212
- recorderActive.current = false;
213
- }
214
- };
215
- }, []);
216
-
217
- useEffect(() => {
218
- let stopFn;
219
- if (!recorderActive.current) {
220
- stopFn = record({
221
- emit(event) {
222
- console.log('Event:', event);
223
- if (event.type == 3 && event.data.type == 2) {
224
- return;
225
- }
226
- if (event.type == 4) {
227
- return;
228
- }
229
- if (event.type == 3 && event.data.source == 0) {
230
- return;
231
- }
232
- if (event.type == 3 && event.data.source == 1) {
233
- return;
234
- }
235
- if (event.type == 3 && event.data.type == 3) {
236
- console.log('Contextmenu Event:', event);
237
- }
238
- setEvents((prevEvents) => [...prevEvents, event]);
239
- localStorage.setItem(
240
- 'coreoutlineFormattedEvents',
241
- JSON.stringify((prevEvents) => [...prevEvents, event])
242
- );
243
- },
244
- maskAllInputs: false,
245
- maskTextSelector: null,
246
- });
247
-
248
- recorderActive.current = true;
249
- }
250
-
251
- return () => {
252
- if (stopFn) {
253
- stopFn();
254
-
255
155
  recorderActive.current = false;
256
156
  }
257
157
  };
@@ -259,180 +159,63 @@ const CoreOutline = ({ children, data_source_id, data_source_secret }) => {
259
159
 
260
160
  useEffect(() => {
261
161
  const handleClick = (event) => {
262
- console.log('Inner text:', event.target.innerText);
263
- const clickEvent = {
264
- type: EventType.IncrementalSnapshot,
265
- data: {
266
- source: IncrementalSource.MouseInteraction,
267
- type: MouseInteractions.Click,
268
- id: event.target.id,
269
- x: event.clientX,
270
- y: event.clientY,
271
- },
272
- timestamp: Date.now(),
273
- };
274
- const clickedElement = document.elementFromPoint(
275
- clickEvent.data.x,
276
- clickEvent.data.y
277
- );
278
-
279
- if (clickedElement) {
280
- clickEvent.data.metadata = {
281
- label:
282
- clickedElement.getAttribute('data-label') ||
283
- clickedElement.innerText ||
284
- null,
285
- value:
286
- clickedElement.getAttribute('value') ||
287
- clickedElement.innerText ||
288
- null,
289
- id:
290
- clickedElement.getAttribute('id') ||
291
- clickedElement.innerText ||
292
- null,
293
- name:
294
- clickedElement.getAttribute('name') ||
295
- clickedElement.innerText ||
296
- null,
297
- class:
298
- clickedElement.getAttribute('class') ||
299
- clickedElement.innerText ||
300
- null,
301
- tag: clickedElement.tagName,
302
- html: String(clickedElement.getHTML()),
303
- innerText: event.target.innerText,
304
- };
305
- }
306
- setEvents((prevEvents) => [...prevEvents, clickEvent]);
307
- localStorage.setItem(
308
- 'coreoutlineFormattedEvents',
309
- JSON.stringify((prevEvents) => [...prevEvents, clickEvent])
310
- );
311
- socket.send({
312
- topic: 'session-data',
313
- data_source_id: data_source_id,
314
- session_id: localStorage.getItem('coreOutlineSessionId'),
315
- start_date: new Date().toLocaleString(),
316
- latitude: location.latitude,
317
- longitude: location.longitude,
318
- browser: browser,
319
- event_id: 'ITEM_CLICKED',
320
- // item_clicked: event.target,
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),
321
186
  });
322
187
  };
323
188
 
324
189
  document.addEventListener('click', handleClick);
325
-
326
- return () => {
327
- document.removeEventListener('click', handleClick);
328
- };
190
+ return () => document.removeEventListener('click', handleClick);
329
191
  }, []);
330
192
 
331
- function saveEventsLocally(type = 'formatted' || 'raw') {
332
- let blob = '';
333
- if (type === 'formatted') {
334
- blob = new Blob([JSON.stringify(events, null, 2)], {
335
- type: 'application/json',
336
- });
337
- } else if (type === 'raw') {
338
- blob = new Blob([JSON.stringify(replayEvents, null, 2)], {
339
- type: 'application/json',
340
- });
341
- }
342
- const url = URL.createObjectURL(blob);
343
- const a = document.createElement('a');
344
- a.href = url;
345
- a.download = `${type}_events.json`;
346
- a.click();
347
- URL.revokeObjectURL(url);
348
- }
349
-
350
- function getBrowserInfo() {
351
- const userAgent = navigator.userAgent;
352
- let browserName = 'Unknown';
353
-
354
- if (userAgent.indexOf('Firefox') > -1) {
355
- browserName = 'Firefox';
356
- } else if (
357
- userAgent.indexOf('Opera') > -1 ||
358
- userAgent.indexOf('OPR') > -1
359
- ) {
360
- browserName = 'Opera';
361
- } else if (userAgent.indexOf('Chrome') > -1) {
362
- browserName = 'Chrome';
363
- } else if (userAgent.indexOf('Safari') > -1) {
364
- browserName = 'Safari';
365
- } else if (
366
- userAgent.indexOf('MSIE') > -1 ||
367
- userAgent.indexOf('Trident/') > -1
368
- ) {
369
- browserName = 'Internet Explorer';
370
- }
371
-
372
- return browserName;
373
- }
374
-
375
- function getLocation() {
376
- if (navigator.geolocation) {
377
- navigator.geolocation.getCurrentPosition(
378
- (position) => {
379
- setLocation({
380
- latitude: position.coords.latitude,
381
- longitude: position.coords.longitude,
382
- });
383
- },
384
- (error) => {
385
- console.error('Error getting location:', error);
386
- }
387
- );
388
- } else {
389
- console.error('Geolocation is not supported by this browser.');
390
- }
391
- }
392
-
393
- return (
394
- <div>
395
- <button
396
- onClick={() => {
397
- saveEventsLocally('formatted');
398
- saveEventsLocally('raw');
399
- }}
400
- >
401
- Save Events Locally
402
- </button>
403
- {children}
404
- </div>
405
- );
193
+ return <>{children}</>;
406
194
  };
407
195
 
408
196
  export const flag_item_clicked = (item_id) => {
409
- const sessionId = localStorage.getItem('coreOutlineSessionId');
410
- const dataSourceId = localStorage.getItem('coreOutlineDataSourceIdId');
411
- console.log('Session', sessionId);
412
- console.log('Data Source', dataSourceId);
413
- socket.send({
414
- topic: 'session-data',
415
- data_source_id: dataSourceId,
416
- session_id: localStorage.getItem('coreOutlineSessionId'),
417
- start_date: new Date().toLocaleString(),
418
- event_id: 'PRODUCT_CLICKED',
419
- 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,
420
205
  });
421
206
  return { status: 'success', message: 'Item click flagged successfully.' };
422
207
  };
423
208
 
424
- export const flag_item_purchased = (item_id) => {
425
- const sessionId = localStorage.getItem('coreOutlineSessionId');
426
- const dataSourceId = localStorage.getItem('coreOutlineDataSourceIdId');
427
- console.log('Session', sessionId);
428
- console.log('Data Source', dataSourceId);
429
- socket.send({
430
- topic: 'session-data',
431
- data_source_id: dataSourceId,
432
- session_id: localStorage.getItem('coreOutlineSessionId'),
433
- start_date: new Date().toLocaleString(),
434
- event_id: 'PRODUCT_PURCHASED',
435
- item_clicked: item_id,
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,
436
219
  });
437
220
  return { status: 'success', message: 'Item purchase flagged successfully.' };
438
221
  };