@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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/assets/index-DCET-swI.js +9 -0
- package/dist/assets/index-dyBLLxDx.css +1 -0
- package/dist/index.html +14 -0
- package/docs/MAINTAINER.md +30 -0
- package/docs/asset-mapping.md +22 -0
- package/docs/limits-and-troubleshooting.md +16 -0
- package/docs/live-behavior.md +20 -0
- package/docs/operator-overview.md +18 -0
- package/docs/setup-auth.md +23 -0
- package/index.html +13 -0
- package/package.json +49 -0
- package/paperclip-plugin.manifest.json +36 -0
- package/src/shared/agent-analytics-client.js +204 -0
- package/src/shared/constants.js +41 -0
- package/src/shared/defaults.js +73 -0
- package/src/shared/live-state.js +384 -0
- package/src/ui/App.jsx +87 -0
- package/src/ui/demo-data.js +238 -0
- package/src/ui/main.jsx +11 -0
- package/src/ui/paperclip-bridge.js +126 -0
- package/src/ui/styles.css +483 -0
- package/src/ui/surfaces/PageSurface.jsx +218 -0
- package/src/ui/surfaces/SettingsSurface.jsx +236 -0
- package/src/ui/surfaces/WidgetSurface.jsx +37 -0
- package/src/worker/index.js +36 -0
- package/src/worker/paperclip.js +37 -0
- package/src/worker/service.js +535 -0
- package/src/worker/state.js +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--aa-bg: #f0e8dc;--aa-bg-deep: #e4d6c0;--aa-ink: #1f2a2a;--aa-muted: #576665;--aa-panel: rgba(255, 250, 242, .78);--aa-panel-stroke: rgba(31, 42, 42, .11);--aa-accent: #0f8b8d;--aa-accent-warm: #dc6f36;--aa-accent-cool: #265f88;--aa-live: #1a9a63;--aa-error: #b24a2f;--aa-shadow: 0 18px 50px rgba(31, 42, 42, .08);--aa-serif: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;--aa-sans: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;--aa-mono: "IBM Plex Mono", "SFMono-Regular", monospace}*{box-sizing:border-box}body{margin:0;font-family:var(--aa-sans);color:var(--aa-ink);background:radial-gradient(circle at top left,rgba(15,139,141,.16),transparent 28%),radial-gradient(circle at top right,rgba(220,111,54,.16),transparent 22%),linear-gradient(180deg,#f8f3eb 0%,var(--aa-bg) 45%,var(--aa-bg-deep) 100%)}a{color:inherit;text-decoration:none}button,input,select{font:inherit}.aa-app{min-height:100vh;padding:24px}.aa-page-shell,.aa-settings-shell{max-width:1400px;margin:0 auto}.aa-hero,.aa-panel,.aa-widget,.aa-metric-card,.aa-asset-card{background:var(--aa-panel);border:1px solid var(--aa-panel-stroke);box-shadow:var(--aa-shadow);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.aa-hero{display:flex;justify-content:space-between;gap:24px;padding:28px;border-radius:28px}.aa-kicker{margin:0 0 8px;font-family:var(--aa-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--aa-muted)}.aa-hero h1,.aa-panel h2,.aa-widget h2{margin:0;font-family:var(--aa-serif);line-height:1}.aa-hero h1{font-size:clamp(2.6rem,5vw,4.3rem);max-width:12ch}.aa-hero-copy{max-width:58ch;color:var(--aa-muted)}.aa-hero-status{min-width:220px;display:flex;flex-direction:column;align-items:flex-end;gap:12px}.aa-metric-grid,.aa-main-grid,.aa-evidence-grid,.aa-asset-grid,.aa-settings-grid,.aa-form-grid{display:grid;gap:18px}.aa-metric-grid{grid-template-columns:repeat(4,minmax(0,1fr));margin:18px 0}.aa-metric-card{border-radius:22px;padding:20px}.aa-metric-card span,.aa-label{display:block;font-size:.82rem;color:var(--aa-muted)}.aa-metric-card strong{display:block;margin-top:10px;font-size:2.3rem;font-family:var(--aa-serif)}.aa-main-grid{grid-template-columns:1.25fr 1fr;align-items:start}.aa-panel,.aa-widget,.aa-asset-card{border-radius:26px;padding:22px}.aa-panel-header,.aa-widget-header,.aa-asset-topline,.aa-widget-footer,.aa-inline-actions,.aa-settings-row{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.aa-world-grid{display:grid;grid-template-columns:minmax(260px,.9fr) 1.1fr;gap:20px;align-items:center}.aa-globe{position:relative;aspect-ratio:1;border-radius:999px;background:radial-gradient(circle at 30% 30%,rgba(15,139,141,.45),transparent 44%),radial-gradient(circle at 70% 65%,rgba(220,111,54,.24),transparent 32%),linear-gradient(180deg,#173838,#0f6465);overflow:hidden;box-shadow:inset 0 0 80px #ffffff26}.aa-globe-ring{position:absolute;inset:18%;border:1px solid rgba(255,255,255,.22);border-radius:999px}.aa-globe-ring-two{inset:8%}.aa-globe-ring-three{inset:32%}.aa-globe-core{position:absolute;inset:38%;display:grid;place-items:center;border-radius:999px;background:#fffaf2eb;color:var(--aa-ink);font-family:var(--aa-mono);letter-spacing:.12em;text-transform:uppercase;font-size:.75rem}.aa-country-list,.aa-feed,.aa-settings-stack{display:grid;gap:12px}.aa-country-row,.aa-feed-row,.aa-mini-row,.aa-settings-row{padding:12px 0;border-top:1px solid rgba(31,42,42,.08)}.aa-country-row:first-child,.aa-feed-row:first-child,.aa-mini-row:first-child,.aa-settings-row:first-child{border-top:0;padding-top:0}.aa-country-row strong,.aa-mini-row strong,.aa-feed-row strong,.aa-settings-row strong{display:block}.aa-country-row span,.aa-feed-row span,.aa-settings-row span{color:var(--aa-muted);font-size:.92rem}.aa-country-bar{width:100%;height:8px;margin-top:8px;border-radius:999px;background:#265f881f;overflow:hidden}.aa-country-bar-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--aa-accent) 0%,var(--aa-accent-warm) 100%)}.aa-world-hot,.aa-status-pill{padding:7px 12px;border-radius:999px;font-size:.78rem;font-family:var(--aa-mono);letter-spacing:.08em;text-transform:uppercase}.aa-world-hot,.aa-status-live,.aa-status-connected,.aa-status-streaming{background:#1a9a6324;color:var(--aa-live)}.aa-status-error,.aa-status-attention{background:#b24a2f24;color:var(--aa-error)}.aa-status-idle,.aa-status-pending,.aa-status-disconnected{background:#265f881f;color:var(--aa-accent-cool)}.aa-mini-panel{background:#ffffff6b;border-radius:20px;padding:18px;border:1px solid rgba(31,42,42,.08)}.aa-mini-panel h3,.aa-assets-section h2{margin:0 0 12px;font-family:var(--aa-serif)}.aa-feed-row{display:flex;justify-content:space-between;gap:12px}.aa-feed-row time{white-space:nowrap;font-size:.85rem;color:var(--aa-muted)}.aa-assets-section{margin-top:18px}.aa-asset-grid{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));margin-top:18px}.aa-asset-card{display:grid;gap:16px}.aa-asset-metrics,.aa-asset-details,.aa-asset-evidence,.aa-widget-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.aa-asset-metrics strong,.aa-widget-metrics strong{display:block;margin-top:8px;font-family:var(--aa-serif);font-size:1.6rem}.aa-button{border:0;border-radius:999px;padding:10px 14px;cursor:pointer;transition:transform .12s ease,opacity .12s ease}.aa-button:hover{transform:translateY(-1px)}.aa-button-primary{background:var(--aa-ink);color:#fff}.aa-button-secondary{background:#0f8b8d1f;color:var(--aa-accent)}.aa-button-ghost{background:transparent;color:var(--aa-muted);border:1px solid rgba(31,42,42,.12)}.aa-widget{max-width:520px;margin:0 auto}.aa-widget-metrics{margin:18px 0}.aa-settings-grid{grid-template-columns:1.15fr .85fr}.aa-form-grid{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.aa-form-grid label,.aa-auth-box label{display:grid;gap:8px;font-size:.92rem;color:var(--aa-muted)}.aa-form-grid input,.aa-form-grid select,.aa-auth-box input{border-radius:14px;border:1px solid rgba(31,42,42,.12);padding:12px 14px;background:#ffffffb8}.aa-auth-box{display:grid;gap:12px;margin-top:16px;padding:16px;border-radius:18px;background:#ffffff7a}.aa-checkbox{display:flex!important;align-items:center;gap:10px}.aa-muted-note{color:var(--aa-muted);font-size:.92rem;display:inline-flex;align-items:center}.aa-panel-warning{border-color:#dc6f3647}@media(max-width:1100px){.aa-metric-grid,.aa-main-grid,.aa-settings-grid,.aa-world-grid{grid-template-columns:1fr}.aa-hero{flex-direction:column}.aa-hero-status{align-items:flex-start}}@media(max-width:720px){.aa-app{padding:14px}.aa-metric-grid,.aa-form-grid,.aa-asset-metrics,.aa-asset-details,.aa-asset-evidence,.aa-widget-metrics{grid-template-columns:1fr 1fr}.aa-feed-row,.aa-panel-header,.aa-widget-header,.aa-widget-footer,.aa-settings-row,.aa-inline-actions{flex-direction:column}}
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Agent Analytics Live</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DCET-swI.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-dyBLLxDx.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
14
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Maintainer Notes
|
|
2
|
+
|
|
3
|
+
## Host contract
|
|
4
|
+
|
|
5
|
+
- Manifest entrypoints:
|
|
6
|
+
- `entrypoints.worker` → `./src/worker/index.js`
|
|
7
|
+
- `entrypoints.ui` → `./dist`
|
|
8
|
+
- Declared surfaces:
|
|
9
|
+
- `page`
|
|
10
|
+
- `dashboardWidget`
|
|
11
|
+
- `settingsPage`
|
|
12
|
+
|
|
13
|
+
## Worker/UI boundary
|
|
14
|
+
|
|
15
|
+
- Worker owns auth, refresh, `/stream`, `/live`, per-company cache, and stream emission
|
|
16
|
+
- UI consumes worker data/actions/streams only
|
|
17
|
+
- No third-party credentials leave the worker boundary
|
|
18
|
+
|
|
19
|
+
## State ownership
|
|
20
|
+
|
|
21
|
+
- Company-scoped config: base URL, live window, poll cadence, enabled mappings
|
|
22
|
+
- Company-scoped auth: access token, refresh token, tier, pending detached login state
|
|
23
|
+
- Company-scoped UI state: snoozed assets
|
|
24
|
+
|
|
25
|
+
## Stream delivery
|
|
26
|
+
|
|
27
|
+
- Worker opens one company-scoped host stream channel
|
|
28
|
+
- Worker emits normalized full-state payloads
|
|
29
|
+
- UI replaces local live state wholesale on each event
|
|
30
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Asset Mapping Guide
|
|
2
|
+
|
|
3
|
+
Mappings are explicit and company-scoped.
|
|
4
|
+
|
|
5
|
+
## Required fields
|
|
6
|
+
|
|
7
|
+
- `assetKey`
|
|
8
|
+
- `label`
|
|
9
|
+
- `kind`
|
|
10
|
+
- `agentAnalyticsProject`
|
|
11
|
+
|
|
12
|
+
## Optional fields
|
|
13
|
+
|
|
14
|
+
- `paperclipProjectId`
|
|
15
|
+
- `primaryHostname`
|
|
16
|
+
- `allowedOrigins`
|
|
17
|
+
- `enabled`
|
|
18
|
+
|
|
19
|
+
## Important v1 rule
|
|
20
|
+
|
|
21
|
+
`/live` snapshots are project-scoped. If multiple mappings reuse the same Agent Analytics project, the plugin may mirror the same live totals across more than one asset card. The settings page warns when this happens.
|
|
22
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Limits And Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Hard limits
|
|
4
|
+
|
|
5
|
+
- `/stream` and `/live` are metered read routes
|
|
6
|
+
- `/live` is paid-only
|
|
7
|
+
- Upstream live streams are capped at 10 concurrent streams per account
|
|
8
|
+
- `/live` keeps only the most recent 5 minutes of backing event history
|
|
9
|
+
|
|
10
|
+
## Common failure modes
|
|
11
|
+
|
|
12
|
+
- `unauthorized` or `invalid refresh token`: reconnect from settings
|
|
13
|
+
- duplicate project mappings: reduce repeated project use or accept mirrored project totals
|
|
14
|
+
- no live data with a free tier: upgrade the connected account
|
|
15
|
+
- stale UI while connection is healthy: check whether the asset is snoozed or disabled
|
|
16
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Live Behavior
|
|
2
|
+
|
|
3
|
+
## Data sources
|
|
4
|
+
|
|
5
|
+
- `/stream`: incremental live event feed over Server-Sent Events
|
|
6
|
+
- `/live`: authoritative rollup snapshot for the short live window
|
|
7
|
+
|
|
8
|
+
## What the metrics mean
|
|
9
|
+
|
|
10
|
+
- `Active visitors`: unique users inside the current `/live` window
|
|
11
|
+
- `Active sessions`: unique sessions inside the current `/live` window
|
|
12
|
+
- `Events / min`: current live-window event rate
|
|
13
|
+
- `Recent events`: operator evidence feed, not a full audit log
|
|
14
|
+
|
|
15
|
+
## What this plugin is not
|
|
16
|
+
|
|
17
|
+
- Not a rebuild of the Agent Analytics reporting product
|
|
18
|
+
- Not a historical dashboard
|
|
19
|
+
- Not an automated issue router
|
|
20
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Operator Overview
|
|
2
|
+
|
|
3
|
+
Agent Analytics Live is the Paperclip surface for answering one question quickly:
|
|
4
|
+
|
|
5
|
+
Which company asset is moving right now, and is that movement worth operator attention?
|
|
6
|
+
|
|
7
|
+
## Surfaces
|
|
8
|
+
|
|
9
|
+
- `page`: company-level live monitor with asset cards, world/country emphasis, top pages, top events, and recent evidence
|
|
10
|
+
- `dashboardWidget`: compact pulse for the main dashboard
|
|
11
|
+
- `settingsPage`: connection, asset mapping, and rollout controls
|
|
12
|
+
|
|
13
|
+
## Assumptions
|
|
14
|
+
|
|
15
|
+
- One Paperclip company connects one Agent Analytics account
|
|
16
|
+
- `/live` and `/stream` are paid live routes, so the account tier must permit live reads
|
|
17
|
+
- The plugin is intentionally live-window-only; it is not historical reporting
|
|
18
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Setup And Auth
|
|
2
|
+
|
|
3
|
+
## Default auth path
|
|
4
|
+
|
|
5
|
+
The plugin is login-first.
|
|
6
|
+
|
|
7
|
+
1. Open the plugin `settingsPage`
|
|
8
|
+
2. Click `Start login`
|
|
9
|
+
3. Open the returned approval URL
|
|
10
|
+
4. Sign in with Google or GitHub
|
|
11
|
+
5. Paste the finish code into the settings page
|
|
12
|
+
6. The worker exchanges the code, stores the session, validates `GET /projects`, and starts live sync
|
|
13
|
+
|
|
14
|
+
## Worker-owned boundary
|
|
15
|
+
|
|
16
|
+
- Access token and refresh token are stored in worker-owned company state
|
|
17
|
+
- The browser UI never receives a raw Agent Analytics API key
|
|
18
|
+
- `/stream` and `/live` are called only from the worker
|
|
19
|
+
|
|
20
|
+
## Compatibility fallback
|
|
21
|
+
|
|
22
|
+
The worker code keeps an internal API-key auth adapter for legacy or self-hosted environments, but v1 does not expose API-key entry in the UI.
|
|
23
|
+
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Agent Analytics Live</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/ui/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-analytics/paperclip-live-analytics-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin Paperclip plugin that exposes live Agent Analytics signals inside a company workspace.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Agent-Analytics/paperclip-live-analytics-plugin.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Agent-Analytics/paperclip-live-analytics-plugin#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Agent-Analytics/paperclip-live-analytics-plugin/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"paperclip",
|
|
17
|
+
"plugin",
|
|
18
|
+
"analytics",
|
|
19
|
+
"live",
|
|
20
|
+
"agent-analytics"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"docs",
|
|
25
|
+
"src",
|
|
26
|
+
"index.html",
|
|
27
|
+
"paperclip-plugin.manifest.json",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "vite build",
|
|
36
|
+
"dev": "vite",
|
|
37
|
+
"test": "node --test tests/*.test.mjs",
|
|
38
|
+
"prepack": "npm test && npm run build",
|
|
39
|
+
"pack:local": "npm pack"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"react": "^19.2.0",
|
|
43
|
+
"react-dom": "^19.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@vitejs/plugin-react": "^5.1.3",
|
|
47
|
+
"vite": "^7.2.4"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "@agent-analytics/paperclip-live-analytics-plugin",
|
|
3
|
+
"name": "Agent Analytics Live",
|
|
4
|
+
"displayName": "Agent Analytics Live",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"categories": ["connector", "ui"],
|
|
7
|
+
"entrypoints": {
|
|
8
|
+
"worker": "./src/worker/index.js",
|
|
9
|
+
"ui": "./dist"
|
|
10
|
+
},
|
|
11
|
+
"surfaces": [
|
|
12
|
+
{
|
|
13
|
+
"type": "page",
|
|
14
|
+
"key": "agentAnalyticsLivePage",
|
|
15
|
+
"title": "Agent Analytics Live"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "dashboardWidget",
|
|
19
|
+
"key": "agentAnalyticsLiveWidget",
|
|
20
|
+
"title": "Agent Analytics Live"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "settingsPage",
|
|
24
|
+
"key": "agentAnalyticsLiveSettings",
|
|
25
|
+
"title": "Agent Analytics Live Settings"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"capabilities": [
|
|
29
|
+
"http.outbound",
|
|
30
|
+
"plugin.state.read",
|
|
31
|
+
"plugin.state.write",
|
|
32
|
+
"companies.read",
|
|
33
|
+
"projects.read"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_SESSION_SCOPES,
|
|
3
|
+
DEFAULT_BASE_URL,
|
|
4
|
+
PLUGIN_DISPLAY_NAME,
|
|
5
|
+
PLUGIN_ID,
|
|
6
|
+
} from './constants.js';
|
|
7
|
+
|
|
8
|
+
function createJsonHeaders(auth) {
|
|
9
|
+
const headers = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
};
|
|
12
|
+
if (auth?.access_token) headers.Authorization = `Bearer ${auth.access_token}`;
|
|
13
|
+
else if (auth?.api_key) headers['X-API-Key'] = auth.api_key;
|
|
14
|
+
return headers;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildQuery(params) {
|
|
18
|
+
return Object.entries(params)
|
|
19
|
+
.filter(([, value]) => value !== null && value !== undefined && value !== '')
|
|
20
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
21
|
+
.join('&');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseSseEvent(rawEvent) {
|
|
25
|
+
const event = {
|
|
26
|
+
event: 'message',
|
|
27
|
+
data: '',
|
|
28
|
+
comment: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const lines = rawEvent.split(/\r?\n/);
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
if (!line) continue;
|
|
34
|
+
if (line.startsWith(':')) {
|
|
35
|
+
event.comment = line.slice(1).trim();
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const separatorIndex = line.indexOf(':');
|
|
39
|
+
const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
40
|
+
const value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1).trimStart();
|
|
41
|
+
if (field === 'event') event.event = value;
|
|
42
|
+
if (field === 'data') event.data = event.data ? `${event.data}\n${value}` : value;
|
|
43
|
+
}
|
|
44
|
+
return event;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AgentAnalyticsClient {
|
|
48
|
+
constructor({
|
|
49
|
+
auth = null,
|
|
50
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
51
|
+
fetchImpl = globalThis.fetch,
|
|
52
|
+
onAuthUpdate = null,
|
|
53
|
+
} = {}) {
|
|
54
|
+
this.auth = auth ? { ...auth } : null;
|
|
55
|
+
this.baseUrl = baseUrl;
|
|
56
|
+
this.fetchImpl = fetchImpl;
|
|
57
|
+
this.onAuthUpdate = onAuthUpdate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setAuth(auth) {
|
|
61
|
+
this.auth = auth ? { ...auth } : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async request(method, path, body, { retryOnRefresh = true } = {}) {
|
|
65
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
66
|
+
method,
|
|
67
|
+
headers: createJsonHeaders(this.auth),
|
|
68
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const data = await response.json().catch(() => ({}));
|
|
72
|
+
|
|
73
|
+
if (response.status === 401 && retryOnRefresh && this.auth?.refresh_token) {
|
|
74
|
+
const refreshed = await this.refreshAgentSession().catch(() => null);
|
|
75
|
+
if (refreshed?.access_token) {
|
|
76
|
+
return this.request(method, path, body, { retryOnRefresh: false });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(data.message || data.error || `HTTP ${response.status}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async startPaperclipAuth({ companyId, label } = {}) {
|
|
88
|
+
return this.request(
|
|
89
|
+
'POST',
|
|
90
|
+
'/agent-sessions/start',
|
|
91
|
+
{
|
|
92
|
+
mode: 'detached',
|
|
93
|
+
client_type: 'paperclip',
|
|
94
|
+
client_name: PLUGIN_DISPLAY_NAME,
|
|
95
|
+
client_instance_id: companyId || null,
|
|
96
|
+
label: label || `Paperclip Company ${companyId || ''}`.trim(),
|
|
97
|
+
scopes: AGENT_SESSION_SCOPES,
|
|
98
|
+
metadata: {
|
|
99
|
+
platform: 'paperclip',
|
|
100
|
+
plugin_id: PLUGIN_ID,
|
|
101
|
+
company_id: companyId || null,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{ retryOnRefresh: false }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async exchangeAgentSession(authRequestId, exchangeCode) {
|
|
109
|
+
return this.request(
|
|
110
|
+
'POST',
|
|
111
|
+
'/agent-sessions/exchange',
|
|
112
|
+
{
|
|
113
|
+
auth_request_id: authRequestId,
|
|
114
|
+
exchange_code: exchangeCode,
|
|
115
|
+
},
|
|
116
|
+
{ retryOnRefresh: false }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async refreshAgentSession() {
|
|
121
|
+
if (!this.auth?.refresh_token) {
|
|
122
|
+
throw new Error('No refresh token available');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const refreshed = await this.request(
|
|
126
|
+
'POST',
|
|
127
|
+
'/agent-sessions/refresh',
|
|
128
|
+
{
|
|
129
|
+
refresh_token: this.auth.refresh_token,
|
|
130
|
+
},
|
|
131
|
+
{ retryOnRefresh: false }
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
this.auth = {
|
|
135
|
+
...this.auth,
|
|
136
|
+
...refreshed.agent_session,
|
|
137
|
+
};
|
|
138
|
+
this.onAuthUpdate?.(this.auth);
|
|
139
|
+
return this.auth;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async listProjects() {
|
|
143
|
+
return this.request('GET', '/projects');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getLive(project, { window } = {}) {
|
|
147
|
+
const query = buildQuery({ project, window });
|
|
148
|
+
return this.request('GET', `/live?${query}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async subscribeToStream({ project, filter, signal, onConnected, onTrack, onComment }) {
|
|
152
|
+
const query = buildQuery({
|
|
153
|
+
project,
|
|
154
|
+
filter,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const response = await this.fetchImpl(`${this.baseUrl}/stream?${query}`, {
|
|
158
|
+
method: 'GET',
|
|
159
|
+
headers: createJsonHeaders(this.auth),
|
|
160
|
+
signal,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!response.ok || !response.body) {
|
|
164
|
+
const payload = await response.text().catch(() => '');
|
|
165
|
+
throw new Error(payload || `Stream failed with HTTP ${response.status}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const reader = response.body.getReader();
|
|
169
|
+
const decoder = new TextDecoder();
|
|
170
|
+
let buffer = '';
|
|
171
|
+
|
|
172
|
+
while (true) {
|
|
173
|
+
const { done, value } = await reader.read();
|
|
174
|
+
if (done) break;
|
|
175
|
+
|
|
176
|
+
buffer += decoder.decode(value, { stream: true });
|
|
177
|
+
let boundary = buffer.indexOf('\n\n');
|
|
178
|
+
while (boundary !== -1) {
|
|
179
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
180
|
+
buffer = buffer.slice(boundary + 2);
|
|
181
|
+
const event = parseSseEvent(rawEvent);
|
|
182
|
+
|
|
183
|
+
if (event.comment) {
|
|
184
|
+
onComment?.(event.comment);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (event.data) {
|
|
188
|
+
let parsed = {};
|
|
189
|
+
try {
|
|
190
|
+
parsed = JSON.parse(event.data);
|
|
191
|
+
} catch {
|
|
192
|
+
parsed = { raw: event.data };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (event.event === 'connected') onConnected?.(parsed);
|
|
196
|
+
if (event.event === 'track') onTrack?.(parsed);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
boundary = buffer.indexOf('\n\n');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const PLUGIN_ID = '@agent-analytics/paperclip-live-analytics-plugin';
|
|
2
|
+
export const PLUGIN_DISPLAY_NAME = 'Agent Analytics Live';
|
|
3
|
+
export const DEFAULT_BASE_URL = 'https://api.agentanalytics.sh';
|
|
4
|
+
export const DEFAULT_LIVE_WINDOW_SECONDS = 60;
|
|
5
|
+
export const DEFAULT_POLL_INTERVAL_SECONDS = 15;
|
|
6
|
+
export const MIN_LIVE_WINDOW_SECONDS = 10;
|
|
7
|
+
export const MAX_LIVE_WINDOW_SECONDS = 300;
|
|
8
|
+
export const MIN_POLL_INTERVAL_SECONDS = 5;
|
|
9
|
+
export const MAX_POLL_INTERVAL_SECONDS = 60;
|
|
10
|
+
export const DEFAULT_SNOOZE_MINUTES = 30;
|
|
11
|
+
export const MAX_ENABLED_ASSET_STREAMS = 10;
|
|
12
|
+
export const LIVE_STREAM_CHANNEL = 'agent-analytics-live';
|
|
13
|
+
export const STATE_NAMESPACE = 'agent-analytics-live';
|
|
14
|
+
|
|
15
|
+
export const DATA_KEYS = {
|
|
16
|
+
livePageLoad: 'live.page.load',
|
|
17
|
+
liveWidgetLoad: 'live.widget.load',
|
|
18
|
+
settingsLoad: 'settings.load',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ACTION_KEYS = {
|
|
22
|
+
authStart: 'auth.start',
|
|
23
|
+
authComplete: 'auth.complete',
|
|
24
|
+
authDisconnect: 'auth.disconnect',
|
|
25
|
+
authReconnect: 'auth.reconnect',
|
|
26
|
+
settingsSave: 'settings.save',
|
|
27
|
+
mappingUpsert: 'mapping.upsert',
|
|
28
|
+
mappingRemove: 'mapping.remove',
|
|
29
|
+
assetSnooze: 'asset.snooze',
|
|
30
|
+
assetUnsnooze: 'asset.unsnooze',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const AGENT_SESSION_SCOPES = [
|
|
34
|
+
'account:read',
|
|
35
|
+
'projects:write',
|
|
36
|
+
'analytics:read',
|
|
37
|
+
'live:read',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const ASSET_KINDS = ['website', 'docs', 'app', 'api', 'other'];
|
|
41
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_BASE_URL,
|
|
3
|
+
DEFAULT_LIVE_WINDOW_SECONDS,
|
|
4
|
+
DEFAULT_POLL_INTERVAL_SECONDS,
|
|
5
|
+
} from './constants.js';
|
|
6
|
+
|
|
7
|
+
export function createDefaultSettings() {
|
|
8
|
+
return {
|
|
9
|
+
agentAnalyticsBaseUrl: DEFAULT_BASE_URL,
|
|
10
|
+
liveWindowSeconds: DEFAULT_LIVE_WINDOW_SECONDS,
|
|
11
|
+
pollIntervalSeconds: DEFAULT_POLL_INTERVAL_SECONDS,
|
|
12
|
+
monitoredAssets: [],
|
|
13
|
+
pluginEnabled: true,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createDefaultAuthState() {
|
|
18
|
+
return {
|
|
19
|
+
mode: 'agent_session',
|
|
20
|
+
accessToken: null,
|
|
21
|
+
refreshToken: null,
|
|
22
|
+
accessExpiresAt: null,
|
|
23
|
+
refreshExpiresAt: null,
|
|
24
|
+
accountSummary: null,
|
|
25
|
+
tier: null,
|
|
26
|
+
status: 'disconnected',
|
|
27
|
+
pendingAuthRequest: null,
|
|
28
|
+
lastValidatedAt: null,
|
|
29
|
+
lastError: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createDefaultSnoozeState() {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createEmptyCompanyLiveState() {
|
|
38
|
+
return {
|
|
39
|
+
type: 'live_state',
|
|
40
|
+
generatedAt: Date.now(),
|
|
41
|
+
pluginEnabled: true,
|
|
42
|
+
authStatus: 'disconnected',
|
|
43
|
+
tier: null,
|
|
44
|
+
account: null,
|
|
45
|
+
connection: {
|
|
46
|
+
status: 'idle',
|
|
47
|
+
label: 'Not connected',
|
|
48
|
+
detail: 'Connect Agent Analytics from settings to start the live feed.',
|
|
49
|
+
},
|
|
50
|
+
metrics: {
|
|
51
|
+
activeVisitors: 0,
|
|
52
|
+
activeSessions: 0,
|
|
53
|
+
eventsPerMinute: 0,
|
|
54
|
+
assetsConfigured: 0,
|
|
55
|
+
assetsVisible: 0,
|
|
56
|
+
countriesTracked: 0,
|
|
57
|
+
},
|
|
58
|
+
world: {
|
|
59
|
+
hotCountry: null,
|
|
60
|
+
countries: [],
|
|
61
|
+
},
|
|
62
|
+
evidence: {
|
|
63
|
+
topPages: [],
|
|
64
|
+
topEvents: [],
|
|
65
|
+
recentEvents: [],
|
|
66
|
+
countries: [],
|
|
67
|
+
},
|
|
68
|
+
assets: [],
|
|
69
|
+
snoozedAssets: [],
|
|
70
|
+
warnings: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|