@agent-analytics/paperclip-live-analytics-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,384 @@
1
+ import {
2
+ ASSET_KINDS,
3
+ DEFAULT_POLL_INTERVAL_SECONDS,
4
+ DEFAULT_SNOOZE_MINUTES,
5
+ DEFAULT_LIVE_WINDOW_SECONDS,
6
+ MAX_ENABLED_ASSET_STREAMS,
7
+ MAX_LIVE_WINDOW_SECONDS,
8
+ MAX_POLL_INTERVAL_SECONDS,
9
+ MIN_LIVE_WINDOW_SECONDS,
10
+ MIN_POLL_INTERVAL_SECONDS,
11
+ } from './constants.js';
12
+ import { createEmptyCompanyLiveState } from './defaults.js';
13
+
14
+ function clampNumber(value, min, max, fallback) {
15
+ const parsed = Number(value);
16
+ if (!Number.isFinite(parsed)) return fallback;
17
+ return Math.min(max, Math.max(min, Math.round(parsed)));
18
+ }
19
+
20
+ function sortCountRows(rows, countKey) {
21
+ return [...rows].sort((left, right) => {
22
+ if ((right[countKey] || 0) !== (left[countKey] || 0)) {
23
+ return (right[countKey] || 0) - (left[countKey] || 0);
24
+ }
25
+ return String(left.label || left.path || left.event || '').localeCompare(
26
+ String(right.label || right.path || right.event || '')
27
+ );
28
+ });
29
+ }
30
+
31
+ function sortCountries(rows) {
32
+ return [...rows].sort((left, right) => {
33
+ if ((right.visitors || 0) !== (left.visitors || 0)) return (right.visitors || 0) - (left.visitors || 0);
34
+ if ((right.sessions || 0) !== (left.sessions || 0)) return (right.sessions || 0) - (left.sessions || 0);
35
+ if ((right.events || 0) !== (left.events || 0)) return (right.events || 0) - (left.events || 0);
36
+ return String(left.country || '').localeCompare(String(right.country || ''));
37
+ });
38
+ }
39
+
40
+ export function clampLiveWindowSeconds(value) {
41
+ return clampNumber(value, MIN_LIVE_WINDOW_SECONDS, MAX_LIVE_WINDOW_SECONDS, DEFAULT_LIVE_WINDOW_SECONDS);
42
+ }
43
+
44
+ export function clampPollIntervalSeconds(value) {
45
+ return clampNumber(value, MIN_POLL_INTERVAL_SECONDS, MAX_POLL_INTERVAL_SECONDS, DEFAULT_POLL_INTERVAL_SECONDS);
46
+ }
47
+
48
+ export function slugifyAssetKey(value) {
49
+ return String(value || '')
50
+ .trim()
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, '-')
53
+ .replace(/^-+|-+$/g, '')
54
+ .slice(0, 64);
55
+ }
56
+
57
+ export function normalizeAssetMapping(input = {}) {
58
+ const label = String(input.label || '').trim();
59
+ const assetKey = slugifyAssetKey(input.assetKey || label || 'asset');
60
+ const kind = ASSET_KINDS.includes(input.kind) ? input.kind : 'other';
61
+ const project = String(input.agentAnalyticsProject || '').trim();
62
+ return {
63
+ assetKey,
64
+ label: label || assetKey,
65
+ kind,
66
+ paperclipProjectId: input.paperclipProjectId ? String(input.paperclipProjectId).trim() : '',
67
+ agentAnalyticsProject: project,
68
+ primaryHostname: input.primaryHostname ? String(input.primaryHostname).trim() : '',
69
+ allowedOrigins: Array.isArray(input.allowedOrigins)
70
+ ? input.allowedOrigins.map((value) => String(value).trim()).filter(Boolean)
71
+ : [],
72
+ enabled: input.enabled !== false,
73
+ };
74
+ }
75
+
76
+ export function buildStreamFilter(mapping) {
77
+ const clauses = [];
78
+ if (mapping.primaryHostname) clauses.push(`hostname:${mapping.primaryHostname}`);
79
+ return clauses.join(';') || null;
80
+ }
81
+
82
+ export function mappingSignature(mapping) {
83
+ return JSON.stringify({
84
+ project: mapping.agentAnalyticsProject,
85
+ filter: buildStreamFilter(mapping),
86
+ });
87
+ }
88
+
89
+ export function createEmptyAssetState(mapping, now = Date.now()) {
90
+ return {
91
+ assetKey: mapping.assetKey,
92
+ label: mapping.label,
93
+ kind: mapping.kind,
94
+ paperclipProjectId: mapping.paperclipProjectId || '',
95
+ agentAnalyticsProject: mapping.agentAnalyticsProject,
96
+ primaryHostname: mapping.primaryHostname || '',
97
+ allowedOrigins: mapping.allowedOrigins || [],
98
+ enabled: mapping.enabled !== false,
99
+ status: 'idle',
100
+ lastUpdatedAt: now,
101
+ lastSnapshotAt: null,
102
+ lastPolledAt: null,
103
+ lastHotCountry: null,
104
+ activeVisitors: 0,
105
+ activeSessions: 0,
106
+ eventsPerMinute: 0,
107
+ topPages: [],
108
+ topEvents: [],
109
+ countries: [],
110
+ recentEvents: [],
111
+ warnings: [],
112
+ errors: [],
113
+ };
114
+ }
115
+
116
+ export function normalizeLiveSnapshot(snapshot = {}) {
117
+ return {
118
+ project: snapshot.project || '',
119
+ windowSeconds: clampLiveWindowSeconds(snapshot.window_seconds),
120
+ timestamp: snapshot.timestamp || Date.now(),
121
+ activeVisitors: Number(snapshot.active_visitors || 0),
122
+ activeSessions: Number(snapshot.active_sessions || 0),
123
+ eventsPerMinute: Number(snapshot.events_per_minute || 0),
124
+ topPages: (snapshot.top_pages || []).map((row) => ({
125
+ path: String(row.path || ''),
126
+ visitors: Number(row.visitors || 0),
127
+ })),
128
+ topEvents: (snapshot.top_events || []).map((row) => ({
129
+ event: String(row.event || ''),
130
+ count: Number(row.count || 0),
131
+ })),
132
+ countries: sortCountries(
133
+ (snapshot.countries || []).map((row) => ({
134
+ country: String(row.country || ''),
135
+ visitors: Number(row.visitors || 0),
136
+ sessions: Number(row.sessions || 0),
137
+ events: Number(row.events || 0),
138
+ }))
139
+ ),
140
+ recentEvents: (snapshot.recent_events || []).map((row, index) => ({
141
+ id: `${row.session_id || row.user_id || row.timestamp || 'event'}-${index}`,
142
+ event: String(row.event || ''),
143
+ properties: row.properties || {},
144
+ userId: row.user_id || null,
145
+ sessionId: row.session_id || null,
146
+ timestamp: Number(row.timestamp || Date.now()),
147
+ country: row.country || null,
148
+ path: row.properties?.path || null,
149
+ assetKey: null,
150
+ assetLabel: null,
151
+ })),
152
+ };
153
+ }
154
+
155
+ export function normalizeTrackEvent(track = {}, mapping) {
156
+ return {
157
+ id: `${track.session_id || track.user_id || track.timestamp || Date.now()}-${track.event || 'track'}`,
158
+ event: String(track.event || ''),
159
+ properties: track.properties || {},
160
+ userId: track.user_id || null,
161
+ sessionId: track.session_id || null,
162
+ timestamp: Number(track.timestamp || Date.now()),
163
+ country: track.country || null,
164
+ path: track.properties?.path || null,
165
+ assetKey: mapping.assetKey,
166
+ assetLabel: mapping.label,
167
+ };
168
+ }
169
+
170
+ export function applySnapshotToAssetState(currentState, snapshot, mapping, now = Date.now()) {
171
+ const normalized = normalizeLiveSnapshot(snapshot);
172
+ return {
173
+ ...createEmptyAssetState(mapping, now),
174
+ ...currentState,
175
+ ...mapping,
176
+ status: 'live',
177
+ lastUpdatedAt: now,
178
+ lastSnapshotAt: normalized.timestamp,
179
+ lastPolledAt: now,
180
+ activeVisitors: normalized.activeVisitors,
181
+ activeSessions: normalized.activeSessions,
182
+ eventsPerMinute: normalized.eventsPerMinute,
183
+ topPages: normalized.topPages,
184
+ topEvents: normalized.topEvents,
185
+ countries: normalized.countries,
186
+ recentEvents: normalized.recentEvents.map((event) => ({
187
+ ...event,
188
+ assetKey: mapping.assetKey,
189
+ assetLabel: mapping.label,
190
+ })),
191
+ errors: [],
192
+ };
193
+ }
194
+
195
+ export function applyTrackEventToAssetState(currentState, rawTrackEvent, mapping, now = Date.now()) {
196
+ const state = currentState || createEmptyAssetState(mapping, now);
197
+ const trackEvent = normalizeTrackEvent(rawTrackEvent, mapping);
198
+ const next = {
199
+ ...state,
200
+ ...mapping,
201
+ status: state.status === 'idle' ? 'streaming' : state.status,
202
+ lastUpdatedAt: now,
203
+ lastHotCountry: trackEvent.country || state.lastHotCountry,
204
+ recentEvents: [trackEvent, ...state.recentEvents].slice(0, 12),
205
+ };
206
+
207
+ if (trackEvent.path) {
208
+ const topPages = [...next.topPages];
209
+ const existing = topPages.find((row) => row.path === trackEvent.path);
210
+ if (existing) existing.visitors += 1;
211
+ else topPages.push({ path: trackEvent.path, visitors: 1 });
212
+ next.topPages = sortCountRows(topPages, 'visitors').slice(0, 10);
213
+ }
214
+
215
+ if (trackEvent.event) {
216
+ const topEvents = [...next.topEvents];
217
+ const existing = topEvents.find((row) => row.event === trackEvent.event);
218
+ if (existing) existing.count += 1;
219
+ else topEvents.push({ event: trackEvent.event, count: 1 });
220
+ next.topEvents = sortCountRows(topEvents, 'count').slice(0, 10);
221
+ }
222
+
223
+ if (trackEvent.country) {
224
+ const countries = [...next.countries];
225
+ const existing = countries.find((row) => row.country === trackEvent.country);
226
+ if (existing) {
227
+ existing.events += 1;
228
+ if (trackEvent.userId && existing.visitors === 0) existing.visitors = 1;
229
+ if (trackEvent.sessionId && existing.sessions === 0) existing.sessions = 1;
230
+ } else {
231
+ countries.push({
232
+ country: trackEvent.country,
233
+ visitors: trackEvent.userId ? 1 : 0,
234
+ sessions: trackEvent.sessionId ? 1 : 0,
235
+ events: 1,
236
+ });
237
+ }
238
+ next.countries = sortCountries(countries).slice(0, 12);
239
+ }
240
+
241
+ next.eventsPerMinute = Math.max(next.eventsPerMinute, next.topEvents.reduce((sum, row) => sum + row.count, 0));
242
+ return next;
243
+ }
244
+
245
+ export function createSnoozeExpiry(minutes = DEFAULT_SNOOZE_MINUTES, now = Date.now()) {
246
+ return now + clampNumber(minutes, 1, 240, DEFAULT_SNOOZE_MINUTES) * 60_000;
247
+ }
248
+
249
+ export function isSnoozed(assetKey, snoozes = {}, now = Date.now()) {
250
+ const snoozeUntil = Number(snoozes[assetKey] || 0);
251
+ return Number.isFinite(snoozeUntil) && snoozeUntil > now;
252
+ }
253
+
254
+ export function validateEnabledMappings(mappings = []) {
255
+ const enabled = mappings.filter((mapping) => mapping.enabled !== false);
256
+ const warnings = [];
257
+ const errors = [];
258
+
259
+ if (enabled.length > MAX_ENABLED_ASSET_STREAMS) {
260
+ errors.push(`Enable at most ${MAX_ENABLED_ASSET_STREAMS} assets at once because Agent Analytics live streams are capped per account.`);
261
+ }
262
+
263
+ const byProject = new Map();
264
+ for (const mapping of enabled) {
265
+ if (!mapping.agentAnalyticsProject) {
266
+ errors.push(`Asset "${mapping.label}" is missing an Agent Analytics project.`);
267
+ continue;
268
+ }
269
+ const count = byProject.get(mapping.agentAnalyticsProject) || 0;
270
+ byProject.set(mapping.agentAnalyticsProject, count + 1);
271
+ }
272
+
273
+ for (const [project, count] of byProject.entries()) {
274
+ if (count > 1) {
275
+ warnings.push(`Project "${project}" is mapped multiple times. /live snapshots are project-scoped, so duplicate mappings can mirror the same activity.`);
276
+ }
277
+ }
278
+
279
+ return { warnings, errors };
280
+ }
281
+
282
+ export function buildCompanyLiveState({ settings, auth, assets, snoozes = {}, now = Date.now() }) {
283
+ const liveState = createEmptyCompanyLiveState();
284
+ const visibleAssets = [];
285
+ const topPagesMap = new Map();
286
+ const topEventsMap = new Map();
287
+ const countryMap = new Map();
288
+ const recentEvents = [];
289
+
290
+ let activeVisitors = 0;
291
+ let activeSessions = 0;
292
+ let eventsPerMinute = 0;
293
+
294
+ for (const asset of assets) {
295
+ if (isSnoozed(asset.assetKey, snoozes, now)) continue;
296
+ visibleAssets.push(asset);
297
+ activeVisitors += asset.activeVisitors || 0;
298
+ activeSessions += asset.activeSessions || 0;
299
+ eventsPerMinute += asset.eventsPerMinute || 0;
300
+
301
+ for (const page of asset.topPages || []) {
302
+ const current = topPagesMap.get(page.path) || 0;
303
+ topPagesMap.set(page.path, current + (page.visitors || 0));
304
+ }
305
+
306
+ for (const event of asset.topEvents || []) {
307
+ const current = topEventsMap.get(event.event) || 0;
308
+ topEventsMap.set(event.event, current + (event.count || 0));
309
+ }
310
+
311
+ for (const country of asset.countries || []) {
312
+ const current = countryMap.get(country.country) || { country: country.country, visitors: 0, sessions: 0, events: 0 };
313
+ current.visitors += country.visitors || 0;
314
+ current.sessions += country.sessions || 0;
315
+ current.events += country.events || 0;
316
+ countryMap.set(country.country, current);
317
+ }
318
+
319
+ for (const event of asset.recentEvents || []) {
320
+ recentEvents.push(event);
321
+ }
322
+ }
323
+
324
+ const warnings = validateEnabledMappings(settings.monitoredAssets).warnings;
325
+ const countries = sortCountries(Array.from(countryMap.values())).slice(0, 12);
326
+ const sortedAssets = [...visibleAssets].sort((left, right) => {
327
+ if ((right.eventsPerMinute || 0) !== (left.eventsPerMinute || 0)) return (right.eventsPerMinute || 0) - (left.eventsPerMinute || 0);
328
+ if ((right.activeVisitors || 0) !== (left.activeVisitors || 0)) return (right.activeVisitors || 0) - (left.activeVisitors || 0);
329
+ return String(left.label || '').localeCompare(String(right.label || ''));
330
+ });
331
+
332
+ liveState.generatedAt = now;
333
+ liveState.pluginEnabled = settings.pluginEnabled !== false;
334
+ liveState.authStatus = auth.status;
335
+ liveState.tier = auth.tier;
336
+ liveState.account = auth.accountSummary;
337
+ liveState.connection = {
338
+ status: auth.status === 'connected' ? 'live' : auth.status === 'error' ? 'error' : 'idle',
339
+ label: auth.status === 'connected' ? 'Connected' : auth.status === 'error' ? 'Attention needed' : 'Not connected',
340
+ detail: auth.status === 'connected'
341
+ ? `Showing ${sortedAssets.length} visible asset${sortedAssets.length === 1 ? '' : 's'} from the current live window.`
342
+ : auth.lastError || 'Connect Agent Analytics from settings to start the live feed.',
343
+ };
344
+ liveState.metrics = {
345
+ activeVisitors,
346
+ activeSessions,
347
+ eventsPerMinute,
348
+ assetsConfigured: settings.monitoredAssets.length,
349
+ assetsVisible: sortedAssets.length,
350
+ countriesTracked: countries.length,
351
+ };
352
+ liveState.world = {
353
+ hotCountry: sortedAssets.find((asset) => asset.lastHotCountry)?.lastHotCountry || countries[0]?.country || null,
354
+ countries,
355
+ };
356
+ liveState.evidence = {
357
+ topPages: sortCountRows(
358
+ Array.from(topPagesMap.entries()).map(([path, visitors]) => ({ path, visitors })),
359
+ 'visitors'
360
+ ).slice(0, 10),
361
+ topEvents: sortCountRows(
362
+ Array.from(topEventsMap.entries()).map(([event, count]) => ({ event, count })),
363
+ 'count'
364
+ ).slice(0, 10),
365
+ recentEvents: recentEvents.sort((left, right) => (right.timestamp || 0) - (left.timestamp || 0)).slice(0, 20),
366
+ countries,
367
+ };
368
+ liveState.assets = sortedAssets;
369
+ liveState.snoozedAssets = Object.keys(snoozes).filter((assetKey) => isSnoozed(assetKey, snoozes, now));
370
+ liveState.warnings = warnings;
371
+ return liveState;
372
+ }
373
+
374
+ export function deriveWidgetSummary(companyLiveState) {
375
+ return {
376
+ connection: companyLiveState.connection,
377
+ tier: companyLiveState.tier,
378
+ metrics: companyLiveState.metrics,
379
+ topAsset: companyLiveState.assets[0] || null,
380
+ hotCountry: companyLiveState.world.hotCountry,
381
+ warnings: companyLiveState.warnings,
382
+ };
383
+ }
384
+
package/src/ui/App.jsx ADDED
@@ -0,0 +1,87 @@
1
+ import { startTransition, useEffect, useState } from 'react';
2
+ import { ACTION_KEYS, DATA_KEYS, LIVE_STREAM_CHANNEL } from '../shared/constants.js';
3
+ import { demoLiveState } from './demo-data.js';
4
+ import { useHostContext, usePluginAction, usePluginData, usePluginStream } from './paperclip-bridge.js';
5
+ import { PageSurface } from './surfaces/PageSurface.jsx';
6
+ import { SettingsSurface } from './surfaces/SettingsSurface.jsx';
7
+ import { WidgetSurface } from './surfaces/WidgetSurface.jsx';
8
+
9
+ function SurfaceFrame({ surface, children }) {
10
+ return (
11
+ <main className={`aa-app aa-surface-${surface}`}>
12
+ {children}
13
+ </main>
14
+ );
15
+ }
16
+
17
+ export function App() {
18
+ const host = useHostContext();
19
+ const surface = host.surface || 'page';
20
+ const companyId = host.companyId;
21
+
22
+ const livePage = usePluginData(DATA_KEYS.livePageLoad, { companyId }, { enabled: surface === 'page' });
23
+ const liveWidget = usePluginData(DATA_KEYS.liveWidgetLoad, { companyId }, { enabled: surface === 'dashboardWidget' });
24
+ const settings = usePluginData(DATA_KEYS.settingsLoad, { companyId }, { enabled: surface === 'settingsPage' });
25
+
26
+ const authStart = usePluginAction(ACTION_KEYS.authStart);
27
+ const authComplete = usePluginAction(ACTION_KEYS.authComplete);
28
+ const authReconnect = usePluginAction(ACTION_KEYS.authReconnect);
29
+ const authDisconnect = usePluginAction(ACTION_KEYS.authDisconnect);
30
+ const settingsSave = usePluginAction(ACTION_KEYS.settingsSave);
31
+ const mappingUpsert = usePluginAction(ACTION_KEYS.mappingUpsert);
32
+ const mappingRemove = usePluginAction(ACTION_KEYS.mappingRemove);
33
+ const snoozeAsset = usePluginAction(ACTION_KEYS.assetSnooze);
34
+ const unsnoozeAsset = usePluginAction(ACTION_KEYS.assetUnsnooze);
35
+
36
+ const [streamState, setStreamState] = useState(demoLiveState);
37
+
38
+ useEffect(() => {
39
+ if (surface === 'page' && livePage.data) setStreamState(livePage.data);
40
+ }, [surface, livePage.data]);
41
+
42
+ usePluginStream(LIVE_STREAM_CHANNEL, {
43
+ companyId,
44
+ onEvent: (payload) => {
45
+ startTransition(() => {
46
+ setStreamState(payload);
47
+ });
48
+ },
49
+ });
50
+
51
+ if (surface === 'dashboardWidget') {
52
+ return (
53
+ <SurfaceFrame surface={surface}>
54
+ <WidgetSurface widget={liveWidget.data || liveWidget.data || { connection: { status: 'idle', label: 'Idle' }, metrics: {}, warnings: [] }} fullPageHref="?surface=page" />
55
+ </SurfaceFrame>
56
+ );
57
+ }
58
+
59
+ if (surface === 'settingsPage') {
60
+ return (
61
+ <SurfaceFrame surface={surface}>
62
+ <SettingsSurface
63
+ settingsData={settings.data}
64
+ onStartAuth={() => authStart.run({ companyId })}
65
+ onCompleteAuth={(authRequestId, exchangeCode) => authComplete.run({ companyId, authRequestId, exchangeCode })}
66
+ onReconnect={() => authReconnect.run({ companyId })}
67
+ onDisconnect={() => authDisconnect.run({ companyId })}
68
+ onSaveSettings={(nextSettings) => settingsSave.run({ companyId, settings: nextSettings })}
69
+ onUpsertMapping={(mapping) => mappingUpsert.run({ companyId, mapping })}
70
+ onRemoveMapping={(assetKey) => mappingRemove.run({ companyId, assetKey })}
71
+ />
72
+ </SurfaceFrame>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <SurfaceFrame surface={surface}>
78
+ <PageSurface
79
+ liveState={streamState}
80
+ basePath={host.basePath}
81
+ onSnooze={(assetKey) => snoozeAsset.run({ companyId, assetKey })}
82
+ onUnsnooze={(assetKey) => unsnoozeAsset.run({ companyId, assetKey })}
83
+ />
84
+ </SurfaceFrame>
85
+ );
86
+ }
87
+
@@ -0,0 +1,238 @@
1
+ import {
2
+ DEFAULT_BASE_URL,
3
+ DEFAULT_LIVE_WINDOW_SECONDS,
4
+ DEFAULT_POLL_INTERVAL_SECONDS,
5
+ } from '../shared/constants.js';
6
+
7
+ const now = Date.now();
8
+
9
+ export const demoLiveState = {
10
+ type: 'live_state',
11
+ generatedAt: now,
12
+ pluginEnabled: true,
13
+ authStatus: 'connected',
14
+ tier: 'pro',
15
+ account: {
16
+ id: 'acct_demo',
17
+ email: 'operator@example.com',
18
+ githubLogin: 'operator',
19
+ googleName: 'Operator Demo',
20
+ },
21
+ connection: {
22
+ status: 'live',
23
+ label: 'Connected',
24
+ detail: 'Showing 3 visible assets from the current live window.',
25
+ },
26
+ metrics: {
27
+ activeVisitors: 29,
28
+ activeSessions: 34,
29
+ eventsPerMinute: 71,
30
+ assetsConfigured: 3,
31
+ assetsVisible: 3,
32
+ countriesTracked: 5,
33
+ },
34
+ world: {
35
+ hotCountry: 'US',
36
+ countries: [
37
+ { country: 'US', visitors: 12, sessions: 13, events: 31 },
38
+ { country: 'DE', visitors: 5, sessions: 6, events: 13 },
39
+ { country: 'GB', visitors: 4, sessions: 5, events: 11 },
40
+ { country: 'IN', visitors: 4, sessions: 5, events: 10 },
41
+ { country: 'IL', visitors: 4, sessions: 5, events: 6 },
42
+ ],
43
+ },
44
+ evidence: {
45
+ topPages: [
46
+ { path: '/pricing', visitors: 12 },
47
+ { path: '/docs/installation', visitors: 8 },
48
+ { path: '/signup', visitors: 5 },
49
+ ],
50
+ topEvents: [
51
+ { event: 'page_view', count: 44 },
52
+ { event: 'signup', count: 12 },
53
+ { event: 'api_key_created', count: 5 },
54
+ ],
55
+ recentEvents: [
56
+ {
57
+ id: 'evt_1',
58
+ event: 'signup',
59
+ path: '/signup',
60
+ country: 'US',
61
+ userId: 'anon_a12',
62
+ sessionId: 'sess_1',
63
+ timestamp: now - 15_000,
64
+ assetKey: 'marketing-site',
65
+ assetLabel: 'Marketing Site',
66
+ },
67
+ {
68
+ id: 'evt_2',
69
+ event: 'page_view',
70
+ path: '/docs/installation',
71
+ country: 'DE',
72
+ userId: 'anon_a18',
73
+ sessionId: 'sess_2',
74
+ timestamp: now - 32_000,
75
+ assetKey: 'docs',
76
+ assetLabel: 'Docs',
77
+ },
78
+ {
79
+ id: 'evt_3',
80
+ event: 'api_key_created',
81
+ path: '/settings/api',
82
+ country: 'GB',
83
+ userId: 'anon_a20',
84
+ sessionId: 'sess_3',
85
+ timestamp: now - 44_000,
86
+ assetKey: 'dashboard-app',
87
+ assetLabel: 'Dashboard App',
88
+ },
89
+ ],
90
+ countries: [
91
+ { country: 'US', visitors: 12, sessions: 13, events: 31 },
92
+ { country: 'DE', visitors: 5, sessions: 6, events: 13 },
93
+ { country: 'GB', visitors: 4, sessions: 5, events: 11 },
94
+ ],
95
+ },
96
+ assets: [
97
+ {
98
+ assetKey: 'marketing-site',
99
+ label: 'Marketing Site',
100
+ kind: 'website',
101
+ paperclipProjectId: 'proj_marketing',
102
+ agentAnalyticsProject: 'agentanalytics-sh',
103
+ primaryHostname: 'agentanalytics.sh',
104
+ enabled: true,
105
+ status: 'live',
106
+ lastUpdatedAt: now,
107
+ lastSnapshotAt: now - 15_000,
108
+ lastPolledAt: now - 9_000,
109
+ lastHotCountry: 'US',
110
+ activeVisitors: 12,
111
+ activeSessions: 14,
112
+ eventsPerMinute: 30,
113
+ topPages: [
114
+ { path: '/pricing', visitors: 8 },
115
+ { path: '/', visitors: 4 },
116
+ ],
117
+ topEvents: [
118
+ { event: 'page_view', count: 21 },
119
+ { event: 'signup', count: 6 },
120
+ ],
121
+ countries: [
122
+ { country: 'US', visitors: 6, sessions: 7, events: 15 },
123
+ { country: 'IN', visitors: 3, sessions: 4, events: 7 },
124
+ ],
125
+ recentEvents: [],
126
+ warnings: [],
127
+ errors: [],
128
+ },
129
+ {
130
+ assetKey: 'docs',
131
+ label: 'Docs',
132
+ kind: 'docs',
133
+ paperclipProjectId: 'proj_docs',
134
+ agentAnalyticsProject: 'docs-agentanalytics-sh',
135
+ primaryHostname: 'docs.agentanalytics.sh',
136
+ enabled: true,
137
+ status: 'live',
138
+ lastUpdatedAt: now,
139
+ lastSnapshotAt: now - 12_000,
140
+ lastPolledAt: now - 8_000,
141
+ lastHotCountry: 'DE',
142
+ activeVisitors: 9,
143
+ activeSessions: 10,
144
+ eventsPerMinute: 24,
145
+ topPages: [
146
+ { path: '/guides/paperclip/', visitors: 7 },
147
+ { path: '/installation/claude-code/', visitors: 4 },
148
+ ],
149
+ topEvents: [
150
+ { event: 'page_view', count: 17 },
151
+ { event: 'copy_command', count: 3 },
152
+ ],
153
+ countries: [
154
+ { country: 'DE', visitors: 4, sessions: 4, events: 8 },
155
+ { country: 'GB', visitors: 2, sessions: 3, events: 6 },
156
+ ],
157
+ recentEvents: [],
158
+ warnings: [],
159
+ errors: [],
160
+ },
161
+ {
162
+ assetKey: 'dashboard-app',
163
+ label: 'Dashboard App',
164
+ kind: 'app',
165
+ paperclipProjectId: 'proj_dashboard',
166
+ agentAnalyticsProject: 'app-agentanalytics-sh',
167
+ primaryHostname: 'app.agentanalytics.sh',
168
+ enabled: true,
169
+ status: 'live',
170
+ lastUpdatedAt: now,
171
+ lastSnapshotAt: now - 7_000,
172
+ lastPolledAt: now - 4_000,
173
+ lastHotCountry: 'GB',
174
+ activeVisitors: 8,
175
+ activeSessions: 10,
176
+ eventsPerMinute: 17,
177
+ topPages: [
178
+ { path: '/settings/api', visitors: 3 },
179
+ { path: '/projects', visitors: 2 },
180
+ ],
181
+ topEvents: [
182
+ { event: 'api_key_created', count: 5 },
183
+ { event: 'project_created', count: 2 },
184
+ ],
185
+ countries: [
186
+ { country: 'GB', visitors: 2, sessions: 2, events: 5 },
187
+ { country: 'IL', visitors: 3, sessions: 3, events: 4 },
188
+ ],
189
+ recentEvents: [],
190
+ warnings: [],
191
+ errors: [],
192
+ },
193
+ ],
194
+ snoozedAssets: [],
195
+ warnings: [],
196
+ };
197
+
198
+ export const demoSettingsData = {
199
+ settings: {
200
+ agentAnalyticsBaseUrl: DEFAULT_BASE_URL,
201
+ liveWindowSeconds: DEFAULT_LIVE_WINDOW_SECONDS,
202
+ pollIntervalSeconds: DEFAULT_POLL_INTERVAL_SECONDS,
203
+ pluginEnabled: true,
204
+ monitoredAssets: demoLiveState.assets.map((asset) => ({
205
+ assetKey: asset.assetKey,
206
+ label: asset.label,
207
+ kind: asset.kind,
208
+ paperclipProjectId: asset.paperclipProjectId,
209
+ agentAnalyticsProject: asset.agentAnalyticsProject,
210
+ primaryHostname: asset.primaryHostname,
211
+ allowedOrigins: [],
212
+ enabled: asset.enabled,
213
+ })),
214
+ },
215
+ auth: {
216
+ status: 'connected',
217
+ mode: 'agent_session',
218
+ tier: 'pro',
219
+ accountSummary: demoLiveState.account,
220
+ accessExpiresAt: now + 3_600_000,
221
+ refreshExpiresAt: now + 7_200_000,
222
+ pendingAuthRequest: null,
223
+ lastValidatedAt: now,
224
+ lastError: null,
225
+ connected: true,
226
+ },
227
+ discoveredProjects: [
228
+ { id: 'aa_marketing', name: 'agentanalytics-sh', allowed_origins: '*' },
229
+ { id: 'aa_docs', name: 'docs-agentanalytics-sh', allowed_origins: '*' },
230
+ { id: 'aa_app', name: 'app-agentanalytics-sh', allowed_origins: '*' },
231
+ ],
232
+ validation: {
233
+ warnings: [],
234
+ errors: [],
235
+ },
236
+ projectListError: null,
237
+ };
238
+