@stacksjs/ts-analytics 0.1.6
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/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Stacks Model Type Stub
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Local type definition for Stacks model compatibility
|
|
5
|
+
// This allows analytics models to work without the @stacksjs/types package
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Attribute validation configuration
|
|
9
|
+
*/
|
|
10
|
+
export interface AttributeValidation {
|
|
11
|
+
rule: string
|
|
12
|
+
min?: number
|
|
13
|
+
max?: number
|
|
14
|
+
message?: string | Record<string, string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Model attribute definition
|
|
19
|
+
*/
|
|
20
|
+
export interface ModelAttributeDefinition {
|
|
21
|
+
required?: boolean
|
|
22
|
+
unique?: boolean
|
|
23
|
+
fillable?: boolean
|
|
24
|
+
default?: unknown
|
|
25
|
+
validation?: AttributeValidation
|
|
26
|
+
cast?: string
|
|
27
|
+
comment?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Model traits configuration
|
|
32
|
+
*/
|
|
33
|
+
export interface ModelTraits {
|
|
34
|
+
useUuid?: boolean
|
|
35
|
+
useTtl?: boolean
|
|
36
|
+
useTimestamps?: boolean
|
|
37
|
+
useSoftDeletes?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type KeyFunction = (_item: any) => string
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* DynamoDB key configuration for single-table design
|
|
44
|
+
*/
|
|
45
|
+
export interface DynamoDBKeyConfig {
|
|
46
|
+
pk?: KeyFunction
|
|
47
|
+
sk?: KeyFunction
|
|
48
|
+
gsi1pk?: KeyFunction
|
|
49
|
+
gsi1sk?: KeyFunction
|
|
50
|
+
gsi2pk?: KeyFunction
|
|
51
|
+
gsi2sk?: KeyFunction
|
|
52
|
+
gsi3pk?: KeyFunction
|
|
53
|
+
gsi3sk?: KeyFunction
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stacks Model definition interface
|
|
58
|
+
* Compatible with @stacksjs/types Model
|
|
59
|
+
*/
|
|
60
|
+
export interface Model {
|
|
61
|
+
name: string
|
|
62
|
+
table?: string
|
|
63
|
+
primaryKey?: string | string[]
|
|
64
|
+
autoIncrement?: boolean
|
|
65
|
+
traits?: ModelTraits
|
|
66
|
+
belongsTo?: string[]
|
|
67
|
+
hasMany?: string[]
|
|
68
|
+
hasOne?: string[]
|
|
69
|
+
attributes: Record<string, ModelAttributeDefinition>
|
|
70
|
+
dynamodb?: DynamoDBKeyConfig
|
|
71
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router configuration using bun-router
|
|
3
|
+
*
|
|
4
|
+
* This file defines all API routes for the analytics service.
|
|
5
|
+
* Note: bun-router uses {param} syntax for route parameters, not :param
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Router } from '@stacksjs/bun-router'
|
|
9
|
+
import { preflightResponse, credentialedCorsHeaders, allowedCorsOrigins } from './utils/response'
|
|
10
|
+
|
|
11
|
+
// Import handlers
|
|
12
|
+
import * as stats from './handlers/stats'
|
|
13
|
+
import * as goals from './handlers/goals'
|
|
14
|
+
import * as sessions from './handlers/sessions'
|
|
15
|
+
import * as heatmaps from './handlers/heatmaps'
|
|
16
|
+
import { withReadCache } from './handlers/lib/read-cache'
|
|
17
|
+
import * as performance from './handlers/performance'
|
|
18
|
+
import * as funnels from './handlers/funnels'
|
|
19
|
+
import * as annotations from './handlers/annotations'
|
|
20
|
+
import * as experiments from './handlers/experiments'
|
|
21
|
+
import * as alerts from './handlers/alerts'
|
|
22
|
+
import * as apiKeys from './handlers/api-keys'
|
|
23
|
+
import * as uptime from './handlers/uptime'
|
|
24
|
+
import * as webhooks from './handlers/webhooks'
|
|
25
|
+
import * as team from './handlers/team'
|
|
26
|
+
import * as data from './handlers/data'
|
|
27
|
+
import * as sharing from './handlers/sharing'
|
|
28
|
+
import * as collect from './handlers/collect'
|
|
29
|
+
import * as misc from './handlers/misc'
|
|
30
|
+
import * as auth from './handlers/auth'
|
|
31
|
+
import * as authz from './handlers/authz'
|
|
32
|
+
import * as oauth from './handlers/oauth'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stealth API path mapping
|
|
36
|
+
* These use innocuous names to bypass content blockers
|
|
37
|
+
* /api/p/ = "project" (instead of /api/sites/)
|
|
38
|
+
*/
|
|
39
|
+
const _STEALTH_PATHS = {
|
|
40
|
+
// Analytics data
|
|
41
|
+
stats: 'summary',
|
|
42
|
+
realtime: 'pulse',
|
|
43
|
+
pages: 'content',
|
|
44
|
+
referrers: 'sources',
|
|
45
|
+
devices: 'clients',
|
|
46
|
+
browsers: 'agents',
|
|
47
|
+
os: 'platform',
|
|
48
|
+
countries: 'geo',
|
|
49
|
+
regions: 'area',
|
|
50
|
+
cities: 'locale',
|
|
51
|
+
timeseries: 'series',
|
|
52
|
+
events: 'actions',
|
|
53
|
+
campaigns: 'promo',
|
|
54
|
+
comparison: 'diff',
|
|
55
|
+
// User behavior
|
|
56
|
+
sessions: 'visits',
|
|
57
|
+
flow: 'journey',
|
|
58
|
+
'entry-exit': 'endpoints',
|
|
59
|
+
live: 'now',
|
|
60
|
+
// Heatmaps
|
|
61
|
+
heatmap: 'touch',
|
|
62
|
+
// Errors
|
|
63
|
+
// Performance
|
|
64
|
+
vitals: 'metrics',
|
|
65
|
+
'vitals-trends': 'metrics-trends',
|
|
66
|
+
'performance-budgets': 'budgets',
|
|
67
|
+
// Goals
|
|
68
|
+
goals: 'targets',
|
|
69
|
+
// Funnels
|
|
70
|
+
funnels: 'pipelines',
|
|
71
|
+
// Other
|
|
72
|
+
annotations: 'notes',
|
|
73
|
+
experiments: 'tests',
|
|
74
|
+
alerts: 'notifications',
|
|
75
|
+
'email-reports': 'scheduled',
|
|
76
|
+
'api-keys': 'tokens',
|
|
77
|
+
uptime: 'monitors',
|
|
78
|
+
webhooks: 'hooks',
|
|
79
|
+
team: 'members',
|
|
80
|
+
export: 'download',
|
|
81
|
+
retention: 'storage',
|
|
82
|
+
gdpr: 'privacy',
|
|
83
|
+
insights: 'intel',
|
|
84
|
+
revenue: 'income',
|
|
85
|
+
share: 'link',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create and configure the router with all routes
|
|
90
|
+
*/
|
|
91
|
+
export async function createRouter(): Promise<Router> {
|
|
92
|
+
// Register background jobs on EVERY entrypoint (#168): the Lambda handler
|
|
93
|
+
// imports the router directly and never ran server/index.ts, so
|
|
94
|
+
// POST /api/jobs/tick iterated an empty jobs array in production — rollups,
|
|
95
|
+
// alerts, digests, and webhooks never ran. bootstrapJobs() is idempotent.
|
|
96
|
+
const { bootstrapJobs } = await import('./jobs')
|
|
97
|
+
bootstrapJobs()
|
|
98
|
+
|
|
99
|
+
const router = new Router({
|
|
100
|
+
// Automatically preserve siteId across all dashboard navigation
|
|
101
|
+
queryPreservation: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
preserve: ['siteId'],
|
|
104
|
+
exclude: ['_t', '_cache', 'callback'],
|
|
105
|
+
routes: ['/dashboard', '/dashboard/*'],
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Disable auto file-based routing (all routes are registered explicitly)
|
|
110
|
+
router._fileRoutesInitialized = true
|
|
111
|
+
|
|
112
|
+
// Split-domain credentialed CORS (#118): when CORS_ORIGINS allowlists the
|
|
113
|
+
// dashboard origin, /api/* responses echo it with Allow-Credentials (the
|
|
114
|
+
// wildcard is forbidden for credentialed fetches), OPTIONS preflights are
|
|
115
|
+
// answered, and state-changing /api/* requests from any OTHER browser
|
|
116
|
+
// origin are rejected (CSRF gate — SameSite=None cookies would otherwise
|
|
117
|
+
// ride along; non-browser clients send no Origin and are unaffected).
|
|
118
|
+
await router.use(async (req: any, next: () => Promise<Response>) => {
|
|
119
|
+
const path = new URL(req.url).pathname
|
|
120
|
+
if (!path.startsWith('/api/'))
|
|
121
|
+
return next()
|
|
122
|
+
const cors = credentialedCorsHeaders(req)
|
|
123
|
+
const origin = req.headers.get('origin')
|
|
124
|
+
if (req.method === 'OPTIONS' && cors) {
|
|
125
|
+
return new Response(null, {
|
|
126
|
+
status: 204,
|
|
127
|
+
headers: {
|
|
128
|
+
...cors,
|
|
129
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
130
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Analytics-Token, X-Share-Token, X-Share-Password',
|
|
131
|
+
'Access-Control-Max-Age': '86400',
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
if (!cors && origin && allowedCorsOrigins().length > 0
|
|
136
|
+
&& req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
|
|
137
|
+
return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
|
|
138
|
+
}
|
|
139
|
+
const res = await next()
|
|
140
|
+
if (!cors)
|
|
141
|
+
return res
|
|
142
|
+
const headers = new Headers(res.headers)
|
|
143
|
+
headers.set('Access-Control-Allow-Origin', cors['Access-Control-Allow-Origin'])
|
|
144
|
+
headers.set('Access-Control-Allow-Credentials', 'true')
|
|
145
|
+
headers.append('Vary', 'Origin')
|
|
146
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Global authorization: gate every /api/sites/{id}/* + /api/p/{id}/* route on
|
|
150
|
+
// project membership (no-op unless ANALYTICS_REQUIRE_AUTH=true). See handlers/authz.
|
|
151
|
+
await router.use(async (req: any, next: () => Promise<Response>) => {
|
|
152
|
+
const denied = await authz.siteAuthGuard(req)
|
|
153
|
+
return denied || next()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Catch-all preflight (#118): bun-router answers unmatched OPTIONS with a
|
|
157
|
+
// built-in wildcard BEFORE middleware runs, which breaks credentialed
|
|
158
|
+
// preflights. An explicit '*' OPTIONS route routes them through normal
|
|
159
|
+
// dispatch: allowlisted /api/* origins get the credentialed grant (via the
|
|
160
|
+
// middleware above), other /api/* origins get a bare 204 (browser fails
|
|
161
|
+
// the preflight), and public paths keep the origin-echo behavior.
|
|
162
|
+
await router.options('*', (req: any) => {
|
|
163
|
+
const path = new URL(req.url).pathname
|
|
164
|
+
if (path.startsWith('/api/'))
|
|
165
|
+
return new Response(null, { status: 204 })
|
|
166
|
+
return preflightResponse(req)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Health check
|
|
170
|
+
await router.get('/health', misc.handleHealth)
|
|
171
|
+
|
|
172
|
+
// Favicon (return empty to prevent 404)
|
|
173
|
+
await router.get('/favicon.ico', () => new Response(null, { status: 204 }))
|
|
174
|
+
|
|
175
|
+
// Auth routes (API only — login page served by stx frontend)
|
|
176
|
+
await router.post('/login', auth.handleLogin)
|
|
177
|
+
await router.post('/logout', auth.handleLogout)
|
|
178
|
+
await router.post('/signup', auth.handleSignupForm)
|
|
179
|
+
await router.post('/forgot', auth.handleForgotForm)
|
|
180
|
+
await router.post('/reset', auth.handleResetForm)
|
|
181
|
+
await router.post('/api/auth/signup', auth.handleSignup)
|
|
182
|
+
await router.post('/api/auth/login', auth.handleAuthLogin)
|
|
183
|
+
await router.get('/api/auth/me', auth.handleMe)
|
|
184
|
+
await router.post('/api/auth/logout', auth.handleApiLogout)
|
|
185
|
+
await router.get('/api/auth/verify', auth.handleVerifyEmail)
|
|
186
|
+
await router.post('/api/auth/verify/resend', auth.handleResendVerification)
|
|
187
|
+
await router.post('/api/auth/forgot', auth.handleForgotPassword)
|
|
188
|
+
await router.post('/api/auth/reset', auth.handleResetPassword)
|
|
189
|
+
await router.post('/api/auth/logout-all', auth.handleLogoutAll)
|
|
190
|
+
await router.put('/api/auth/profile', auth.handleUpdateProfile)
|
|
191
|
+
await router.post('/api/auth/password', auth.handleChangePassword)
|
|
192
|
+
await router.post('/api/auth/email', auth.handleChangeEmail)
|
|
193
|
+
await router.get('/api/auth/oauth/{provider}', (req: any) => oauth.handleOAuthStart(req, req.params.provider))
|
|
194
|
+
await router.get('/api/auth/oauth/{provider}/callback', (req: any) => oauth.handleOAuthCallback(req, req.params.provider))
|
|
195
|
+
|
|
196
|
+
// Scheduled-jobs tick — for an external cron (Lambda schedule / EventBridge).
|
|
197
|
+
// Gated by ANALYTICS_JOBS_SECRET; disabled (403) when no secret is set.
|
|
198
|
+
await router.post('/api/jobs/tick', async (req: any) => {
|
|
199
|
+
const secret = process.env.ANALYTICS_JOBS_SECRET
|
|
200
|
+
if (!secret || req.headers.get('x-jobs-secret') !== secret) {
|
|
201
|
+
return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
|
|
202
|
+
}
|
|
203
|
+
const result = await import('./lib/scheduler').then(s => s.runDueJobs())
|
|
204
|
+
return new Response(JSON.stringify(result), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Collection endpoints. Cross-domain installs preflight every beacon (JSON
|
|
208
|
+
// content type; the error SDK also sends X-Analytics-Token), so each public
|
|
209
|
+
// collect endpoint answers OPTIONS (#86).
|
|
210
|
+
await router.post('/collect', collect.handleCollect)
|
|
211
|
+
await router.post('/t', collect.handleCollect)
|
|
212
|
+
for (const path of ['/collect', '/t', '/p']) {
|
|
213
|
+
await router.options(path, (req: any) => preflightResponse(req))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sites list and creation — account-scoped (#114). With enforcement on
|
|
217
|
+
// (the default), no session → 401; the all-sites fallback only survives in
|
|
218
|
+
// explicit ANALYTICS_REQUIRE_AUTH=false installs.
|
|
219
|
+
await router.get('/api/sites', async (req: any) => {
|
|
220
|
+
const session = await auth.getSessionFromRequest(req)
|
|
221
|
+
if (!session && authz.authRequired()) {
|
|
222
|
+
return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
|
|
223
|
+
}
|
|
224
|
+
return misc.handleGetSites(req, session?.userId)
|
|
225
|
+
})
|
|
226
|
+
await router.post('/api/sites', async (req: any) => {
|
|
227
|
+
const session = await auth.getSessionFromRequest(req)
|
|
228
|
+
if (!session && authz.authRequired()) {
|
|
229
|
+
return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
|
|
230
|
+
}
|
|
231
|
+
return misc.handleCreateSite(req, session?.userId)
|
|
232
|
+
})
|
|
233
|
+
await router.delete('/api/sites/{siteId}', async (req: any) => {
|
|
234
|
+
const session = await auth.getSessionFromRequest(req)
|
|
235
|
+
return misc.handleDeleteSite(req, req.params.siteId, session?.userId)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Share link validation
|
|
239
|
+
await router.get('/api/share/{token}', (req: any) => sharing.handleGetSharedDashboard(req, req.params.token))
|
|
240
|
+
|
|
241
|
+
// Site-specific API routes
|
|
242
|
+
// Stats & Analytics
|
|
243
|
+
await router.get('/api/sites/{siteId}/stats', (req: any) => withReadCache(req, 'stats', 30_000, () => stats.handleGetStats(req, req.params.siteId)))
|
|
244
|
+
await router.get('/api/sites/{siteId}/realtime', (req: any) => stats.handleGetRealtime(req, req.params.siteId))
|
|
245
|
+
await router.get('/api/sites/{siteId}/ingest-counters', (req: any) => misc.handleGetIngestCounters(req, req.params.siteId))
|
|
246
|
+
await router.get('/api/sites/{siteId}/pages', (req: any) => withReadCache(req, 'pages', 60_000, () => stats.handleGetPages(req, req.params.siteId)))
|
|
247
|
+
await router.get('/api/sites/{siteId}/referrers', (req: any) => withReadCache(req, 'referrers', 60_000, () => stats.handleGetReferrers(req, req.params.siteId)))
|
|
248
|
+
await router.get('/api/sites/{siteId}/screen-sizes', (req: any) => withReadCache(req, 'screen-sizes', 60_000, () => stats.handleGetScreenSizes(req, req.params.siteId)))
|
|
249
|
+
await router.get('/api/sites/{siteId}/devices', (req: any) => withReadCache(req, 'devices', 60_000, () => stats.handleGetDevices(req, req.params.siteId)))
|
|
250
|
+
await router.get('/api/sites/{siteId}/browsers', (req: any) => withReadCache(req, 'browsers', 60_000, () => stats.handleGetBrowsers(req, req.params.siteId)))
|
|
251
|
+
await router.get('/api/sites/{siteId}/os', (req: any) => withReadCache(req, 'os', 60_000, () => stats.handleGetOS(req, req.params.siteId)))
|
|
252
|
+
await router.get('/api/sites/{siteId}/countries', (req: any) => withReadCache(req, 'countries', 60_000, () => stats.handleGetCountries(req, req.params.siteId)))
|
|
253
|
+
await router.get('/api/sites/{siteId}/regions', (req: any) => withReadCache(req, 'regions', 60_000, () => stats.handleGetRegions(req, req.params.siteId)))
|
|
254
|
+
await router.get('/api/sites/{siteId}/cities', (req: any) => withReadCache(req, 'cities', 60_000, () => stats.handleGetCities(req, req.params.siteId)))
|
|
255
|
+
await router.get('/api/sites/{siteId}/timeseries', (req: any) => withReadCache(req, 'timeseries', 60_000, () => stats.handleGetTimeSeries(req, req.params.siteId)))
|
|
256
|
+
await router.get('/api/sites/{siteId}/events', (req: any) => withReadCache(req, 'events', 60_000, () => stats.handleGetEvents(req, req.params.siteId)))
|
|
257
|
+
await router.get('/api/sites/{siteId}/event-properties', (req: any) => withReadCache(req, 'event-properties', 60_000, () => stats.handleGetEventProperties(req, req.params.siteId)))
|
|
258
|
+
await router.get('/api/sites/{siteId}/clicks', (req: any) => withReadCache(req, 'clicks', 60_000, () => stats.handleGetClicks(req, req.params.siteId)))
|
|
259
|
+
await router.get('/api/sites/{siteId}/engagement', (req: any) => withReadCache(req, 'engagement', 60_000, () => stats.handleGetEngagement(req, req.params.siteId)))
|
|
260
|
+
await router.get('/api/sites/{siteId}/campaigns', (req: any) => withReadCache(req, 'campaigns', 60_000, () => stats.handleGetCampaigns(req, req.params.siteId)))
|
|
261
|
+
await router.get('/api/sites/{siteId}/comparison', (req: any) => withReadCache(req, 'comparison', 60_000, () => stats.handleGetComparison(req, req.params.siteId)))
|
|
262
|
+
|
|
263
|
+
// Goals
|
|
264
|
+
await router.get('/api/sites/{siteId}/goals/stats', (req: any) => withReadCache(req, 'goals-stats', 60_000, () => goals.handleGetGoalStats(req, req.params.siteId)))
|
|
265
|
+
await router.get('/api/sites/{siteId}/goals', (req: any) => goals.handleGetGoals(req, req.params.siteId))
|
|
266
|
+
await router.post('/api/sites/{siteId}/goals', (req: any) => goals.handleCreateGoal(req, req.params.siteId))
|
|
267
|
+
await router.put('/api/sites/{siteId}/goals/{goalId}', (req: any) => goals.handleUpdateGoal(req, req.params.siteId, req.params.goalId))
|
|
268
|
+
await router.delete('/api/sites/{siteId}/goals/{goalId}', (req: any) => goals.handleDeleteGoal(req, req.params.siteId, req.params.goalId))
|
|
269
|
+
|
|
270
|
+
// Sessions
|
|
271
|
+
await router.get('/api/sites/{siteId}/sessions', (req: any) => sessions.handleGetSessions(req, req.params.siteId))
|
|
272
|
+
await router.get('/api/sites/{siteId}/sessions/{sessionId}', (req: any) => sessions.handleGetSessionDetail(req, req.params.siteId, req.params.sessionId))
|
|
273
|
+
await router.get('/api/sites/{siteId}/flow', (req: any) => withReadCache(req, 'flow', 60_000, () => sessions.handleGetUserFlow(req, req.params.siteId)))
|
|
274
|
+
await router.get('/api/sites/{siteId}/entry-exit', (req: any) => withReadCache(req, 'entry-exit', 60_000, () => sessions.handleGetEntryExitPages(req, req.params.siteId)))
|
|
275
|
+
await router.get('/api/sites/{siteId}/live', (req: any) => sessions.handleGetLiveView(req, req.params.siteId))
|
|
276
|
+
|
|
277
|
+
// Heatmaps
|
|
278
|
+
await router.get('/api/sites/{siteId}/heatmap/clicks', (req: any) => heatmaps.handleGetHeatmapClicks(req, req.params.siteId))
|
|
279
|
+
await router.get('/api/sites/{siteId}/heatmap/scroll', (req: any) => heatmaps.handleGetHeatmapScroll(req, req.params.siteId))
|
|
280
|
+
await router.get('/api/sites/{siteId}/heatmap/pages', (req: any) => heatmaps.handleGetHeatmapPages(req, req.params.siteId))
|
|
281
|
+
|
|
282
|
+
// Performance & Vitals
|
|
283
|
+
await router.get('/api/sites/{siteId}/vitals', (req: any) => performance.handleGetVitals(req, req.params.siteId))
|
|
284
|
+
await router.get('/api/sites/{siteId}/vitals-trends', (req: any) => performance.handleGetVitalsTrends(req, req.params.siteId))
|
|
285
|
+
await router.get('/api/sites/{siteId}/performance-budgets', (req: any) => performance.handleGetPerformanceBudgets(req, req.params.siteId))
|
|
286
|
+
await router.get('/api/sites/{siteId}/performance-budgets/check', (req: any) => performance.handleCheckPerformanceBudgets(req, req.params.siteId))
|
|
287
|
+
await router.post('/api/sites/{siteId}/performance-budgets', (req: any) => performance.handleCreatePerformanceBudget(req, req.params.siteId))
|
|
288
|
+
await router.delete('/api/sites/{siteId}/performance-budgets/{budgetId}', (req: any) => performance.handleDeletePerformanceBudget(req, req.params.siteId, req.params.budgetId))
|
|
289
|
+
|
|
290
|
+
// Funnels
|
|
291
|
+
await router.get('/api/sites/{siteId}/funnels', (req: any) => funnels.handleGetFunnels(req, req.params.siteId))
|
|
292
|
+
await router.get('/api/sites/{siteId}/funnels/{funnelId}', (req: any) => funnels.handleGetFunnelAnalysis(req, req.params.siteId, req.params.funnelId))
|
|
293
|
+
await router.post('/api/sites/{siteId}/funnels', (req: any) => funnels.handleCreateFunnel(req, req.params.siteId))
|
|
294
|
+
await router.delete('/api/sites/{siteId}/funnels/{funnelId}', (req: any) => funnels.handleDeleteFunnel(req, req.params.siteId, req.params.funnelId))
|
|
295
|
+
|
|
296
|
+
// Annotations
|
|
297
|
+
await router.get('/api/sites/{siteId}/annotations', (req: any) => annotations.handleGetAnnotations(req, req.params.siteId))
|
|
298
|
+
await router.post('/api/sites/{siteId}/annotations', (req: any) => annotations.handleCreateAnnotation(req, req.params.siteId))
|
|
299
|
+
await router.delete('/api/sites/{siteId}/annotations/{annotationId}', (req: any) => annotations.handleDeleteAnnotation(req, req.params.siteId, req.params.annotationId))
|
|
300
|
+
|
|
301
|
+
// Experiments
|
|
302
|
+
await router.get('/api/sites/{siteId}/experiments', (req: any) => experiments.handleGetExperiments(req, req.params.siteId))
|
|
303
|
+
await router.post('/api/sites/{siteId}/experiments', (req: any) => experiments.handleCreateExperiment(req, req.params.siteId))
|
|
304
|
+
await router.post('/api/sites/{siteId}/experiments/event', (req: any) => experiments.handleRecordExperimentEvent(req, req.params.siteId))
|
|
305
|
+
|
|
306
|
+
// Alerts
|
|
307
|
+
await router.get('/api/sites/{siteId}/alerts', (req: any) => alerts.handleGetAlerts(req, req.params.siteId))
|
|
308
|
+
await router.post('/api/sites/{siteId}/alerts', (req: any) => alerts.handleCreateAlert(req, req.params.siteId))
|
|
309
|
+
await router.delete('/api/sites/{siteId}/alerts/{alertId}', (req: any) => alerts.handleDeleteAlert(req, req.params.siteId, req.params.alertId))
|
|
310
|
+
|
|
311
|
+
// Email Reports
|
|
312
|
+
await router.get('/api/sites/{siteId}/email-reports', (req: any) => alerts.handleGetEmailReports(req, req.params.siteId))
|
|
313
|
+
await router.post('/api/sites/{siteId}/email-reports', (req: any) => alerts.handleCreateEmailReport(req, req.params.siteId))
|
|
314
|
+
await router.delete('/api/sites/{siteId}/email-reports/{reportId}', (req: any) => alerts.handleDeleteEmailReport(req, req.params.siteId, req.params.reportId))
|
|
315
|
+
|
|
316
|
+
// API Keys
|
|
317
|
+
await router.get('/api/sites/{siteId}/api-keys', (req: any) => apiKeys.handleGetApiKey(req, req.params.siteId))
|
|
318
|
+
await router.post('/api/sites/{siteId}/api-keys/regenerate', (req: any) => apiKeys.handleRegenerateApiKey(req, req.params.siteId))
|
|
319
|
+
|
|
320
|
+
// Uptime Monitoring
|
|
321
|
+
await router.get('/api/sites/{siteId}/uptime', (req: any) => uptime.handleGetUptimeMonitors(req, req.params.siteId))
|
|
322
|
+
await router.get('/api/sites/{siteId}/uptime/{monitorId}/history', (req: any) => uptime.handleGetUptimeHistory(req, req.params.siteId, req.params.monitorId))
|
|
323
|
+
await router.post('/api/sites/{siteId}/uptime', (req: any) => uptime.handleCreateUptimeMonitor(req, req.params.siteId))
|
|
324
|
+
await router.delete('/api/sites/{siteId}/uptime/{monitorId}', (req: any) => uptime.handleDeleteUptimeMonitor(req, req.params.siteId, req.params.monitorId))
|
|
325
|
+
|
|
326
|
+
// Webhooks
|
|
327
|
+
await router.get('/api/sites/{siteId}/webhooks', (req: any) => webhooks.handleGetWebhooks(req, req.params.siteId))
|
|
328
|
+
await router.post('/api/sites/{siteId}/webhooks', (req: any) => webhooks.handleCreateWebhook(req, req.params.siteId))
|
|
329
|
+
await router.delete('/api/sites/{siteId}/webhooks/{webhookId}', (req: any) => webhooks.handleDeleteWebhook(req, req.params.siteId, req.params.webhookId))
|
|
330
|
+
|
|
331
|
+
// Team Management
|
|
332
|
+
await router.get('/api/sites/{siteId}/team', (req: any) => team.handleGetTeamMembers(req, req.params.siteId))
|
|
333
|
+
await router.post('/api/sites/{siteId}/team', (req: any) => team.handleInviteTeamMember(req, req.params.siteId))
|
|
334
|
+
await router.delete('/api/sites/{siteId}/team/{memberId}', (req: any) => team.handleRemoveTeamMember(req, req.params.siteId, req.params.memberId))
|
|
335
|
+
|
|
336
|
+
// Data Export & Retention
|
|
337
|
+
await router.post('/api/sites/{siteId}/import/ga4-api', (req: any) => data.handleGa4ApiImport(req, req.params.siteId))
|
|
338
|
+
await router.post('/api/sites/{siteId}/import/ga', (req: any) => data.handleGaImport(req, req.params.siteId))
|
|
339
|
+
await router.get('/api/sites/{siteId}/export', (req: any) => data.handleExport(req, req.params.siteId))
|
|
340
|
+
await router.get('/api/sites/{siteId}/retention', (req: any) => data.handleGetRetentionSettings(req, req.params.siteId))
|
|
341
|
+
await router.put('/api/sites/{siteId}/retention', (req: any) => data.handleUpdateRetentionSettings(req, req.params.siteId))
|
|
342
|
+
|
|
343
|
+
// GDPR
|
|
344
|
+
await router.get('/api/sites/{siteId}/gdpr/export', (req: any) => data.handleGdprExport(req, req.params.siteId))
|
|
345
|
+
await router.post('/api/sites/{siteId}/gdpr/delete', (req: any) => data.handleGdprDelete(req, req.params.siteId))
|
|
346
|
+
|
|
347
|
+
// Insights
|
|
348
|
+
await router.get('/api/sites/{siteId}/insights', (req: any) => withReadCache(req, 'insights', 120_000, () => data.handleGetInsights(req, req.params.siteId)))
|
|
349
|
+
|
|
350
|
+
// Revenue
|
|
351
|
+
await router.get('/api/sites/{siteId}/revenue', (req: any) => misc.handleGetRevenue(req, req.params.siteId))
|
|
352
|
+
await router.get('/api/sites/{siteId}/verify-install', (req: any) => misc.handleVerifyInstall(req, req.params.siteId))
|
|
353
|
+
|
|
354
|
+
// Share Links
|
|
355
|
+
await router.post('/api/sites/{siteId}/share', (req: any) => sharing.handleCreateShareLink(req, req.params.siteId))
|
|
356
|
+
await router.get('/api/sites/{siteId}/share', (req: any) => sharing.handleListShareLinks(req, req.params.siteId))
|
|
357
|
+
await router.delete('/api/sites/{siteId}/share/{token}', (req: any) => sharing.handleRevokeShareLink(req, req.params.siteId, req.params.token))
|
|
358
|
+
|
|
359
|
+
// ============================================
|
|
360
|
+
// STEALTH ROUTES - Bypass content blockers
|
|
361
|
+
// Uses /api/p/ ("project") with innocuous names
|
|
362
|
+
// ============================================
|
|
363
|
+
|
|
364
|
+
// Collection endpoints (stealth)
|
|
365
|
+
await router.post('/t', collect.handleCollect) // Already defined above, but /t is short
|
|
366
|
+
await router.post('/p', collect.handleCollect) // Even shorter alias
|
|
367
|
+
|
|
368
|
+
// Sites list (stealth) — same account scoping as /api/sites (#114)
|
|
369
|
+
await router.get('/api/projects', async (req: any) => {
|
|
370
|
+
const session = await auth.getSessionFromRequest(req)
|
|
371
|
+
if (!session && authz.authRequired()) {
|
|
372
|
+
return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
|
|
373
|
+
}
|
|
374
|
+
return misc.handleGetSites(req, session?.userId)
|
|
375
|
+
})
|
|
376
|
+
await router.post('/api/projects', async (req: any) => {
|
|
377
|
+
const session = await auth.getSessionFromRequest(req)
|
|
378
|
+
if (!session && authz.authRequired()) {
|
|
379
|
+
return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
|
|
380
|
+
}
|
|
381
|
+
return misc.handleCreateSite(req, session?.userId)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// Share link validation (stealth)
|
|
385
|
+
await router.get('/api/link/{token}', (req: any) => sharing.handleGetSharedDashboard(req, req.params.token))
|
|
386
|
+
|
|
387
|
+
// Site-specific stealth routes using /api/p/ prefix
|
|
388
|
+
// Stats & Analytics (stealth)
|
|
389
|
+
await router.get('/api/p/{siteId}/summary', (req: any) => withReadCache(req, 'stats', 30_000, () => stats.handleGetStats(req, req.params.siteId)))
|
|
390
|
+
await router.get('/api/p/{siteId}/pulse', (req: any) => stats.handleGetRealtime(req, req.params.siteId))
|
|
391
|
+
await router.get('/api/p/{siteId}/content', (req: any) => stats.handleGetPages(req, req.params.siteId))
|
|
392
|
+
await router.get('/api/p/{siteId}/sources', (req: any) => stats.handleGetReferrers(req, req.params.siteId))
|
|
393
|
+
await router.get('/api/p/{siteId}/displays', (req: any) => withReadCache(req, 'screen-sizes', 60_000, () => stats.handleGetScreenSizes(req, req.params.siteId)))
|
|
394
|
+
await router.get('/api/p/{siteId}/clients', (req: any) => stats.handleGetDevices(req, req.params.siteId))
|
|
395
|
+
await router.get('/api/p/{siteId}/agents', (req: any) => stats.handleGetBrowsers(req, req.params.siteId))
|
|
396
|
+
await router.get('/api/p/{siteId}/platform', (req: any) => stats.handleGetOS(req, req.params.siteId))
|
|
397
|
+
await router.get('/api/p/{siteId}/geo', (req: any) => stats.handleGetCountries(req, req.params.siteId))
|
|
398
|
+
await router.get('/api/p/{siteId}/area', (req: any) => stats.handleGetRegions(req, req.params.siteId))
|
|
399
|
+
await router.get('/api/p/{siteId}/locale', (req: any) => stats.handleGetCities(req, req.params.siteId))
|
|
400
|
+
await router.get('/api/p/{siteId}/series', (req: any) => withReadCache(req, 'timeseries', 60_000, () => stats.handleGetTimeSeries(req, req.params.siteId)))
|
|
401
|
+
await router.get('/api/p/{siteId}/actions', (req: any) => stats.handleGetEvents(req, req.params.siteId))
|
|
402
|
+
await router.get('/api/p/{siteId}/traits', (req: any) => stats.handleGetEventProperties(req, req.params.siteId))
|
|
403
|
+
await router.get('/api/p/{siteId}/links', (req: any) => stats.handleGetClicks(req, req.params.siteId))
|
|
404
|
+
await router.get('/api/p/{siteId}/dwell', (req: any) => stats.handleGetEngagement(req, req.params.siteId))
|
|
405
|
+
await router.get('/api/p/{siteId}/promo', (req: any) => stats.handleGetCampaigns(req, req.params.siteId))
|
|
406
|
+
await router.get('/api/p/{siteId}/diff', (req: any) => stats.handleGetComparison(req, req.params.siteId))
|
|
407
|
+
|
|
408
|
+
// Goals (stealth)
|
|
409
|
+
await router.get('/api/p/{siteId}/targets/data', (req: any) => goals.handleGetGoalStats(req, req.params.siteId))
|
|
410
|
+
await router.get('/api/p/{siteId}/targets', (req: any) => goals.handleGetGoals(req, req.params.siteId))
|
|
411
|
+
await router.post('/api/p/{siteId}/targets', (req: any) => goals.handleCreateGoal(req, req.params.siteId))
|
|
412
|
+
await router.put('/api/p/{siteId}/targets/{goalId}', (req: any) => goals.handleUpdateGoal(req, req.params.siteId, req.params.goalId))
|
|
413
|
+
await router.delete('/api/p/{siteId}/targets/{goalId}', (req: any) => goals.handleDeleteGoal(req, req.params.siteId, req.params.goalId))
|
|
414
|
+
|
|
415
|
+
// Sessions (stealth)
|
|
416
|
+
await router.get('/api/p/{siteId}/visits', (req: any) => sessions.handleGetSessions(req, req.params.siteId))
|
|
417
|
+
await router.get('/api/p/{siteId}/visits/{sessionId}', (req: any) => sessions.handleGetSessionDetail(req, req.params.siteId, req.params.sessionId))
|
|
418
|
+
await router.get('/api/p/{siteId}/journey', (req: any) => sessions.handleGetUserFlow(req, req.params.siteId))
|
|
419
|
+
await router.get('/api/p/{siteId}/endpoints', (req: any) => sessions.handleGetEntryExitPages(req, req.params.siteId))
|
|
420
|
+
await router.get('/api/p/{siteId}/now', (req: any) => sessions.handleGetLiveView(req, req.params.siteId))
|
|
421
|
+
|
|
422
|
+
// Heatmaps (stealth)
|
|
423
|
+
await router.get('/api/p/{siteId}/touch/clicks', (req: any) => heatmaps.handleGetHeatmapClicks(req, req.params.siteId))
|
|
424
|
+
await router.get('/api/p/{siteId}/touch/scroll', (req: any) => heatmaps.handleGetHeatmapScroll(req, req.params.siteId))
|
|
425
|
+
await router.get('/api/p/{siteId}/touch/list', (req: any) => heatmaps.handleGetHeatmapPages(req, req.params.siteId))
|
|
426
|
+
|
|
427
|
+
// Errors (stealth)
|
|
428
|
+
|
|
429
|
+
// Performance & Vitals (stealth)
|
|
430
|
+
await router.get('/api/p/{siteId}/metrics', (req: any) => performance.handleGetVitals(req, req.params.siteId))
|
|
431
|
+
await router.get('/api/p/{siteId}/metrics-trends', (req: any) => performance.handleGetVitalsTrends(req, req.params.siteId))
|
|
432
|
+
await router.get('/api/p/{siteId}/budgets', (req: any) => performance.handleGetPerformanceBudgets(req, req.params.siteId))
|
|
433
|
+
await router.get('/api/p/{siteId}/budgets/check', (req: any) => performance.handleCheckPerformanceBudgets(req, req.params.siteId))
|
|
434
|
+
await router.post('/api/p/{siteId}/budgets', (req: any) => performance.handleCreatePerformanceBudget(req, req.params.siteId))
|
|
435
|
+
await router.delete('/api/p/{siteId}/budgets/{budgetId}', (req: any) => performance.handleDeletePerformanceBudget(req, req.params.siteId, req.params.budgetId))
|
|
436
|
+
|
|
437
|
+
// Funnels (stealth)
|
|
438
|
+
await router.get('/api/p/{siteId}/pipelines', (req: any) => funnels.handleGetFunnels(req, req.params.siteId))
|
|
439
|
+
await router.get('/api/p/{siteId}/pipelines/{funnelId}', (req: any) => funnels.handleGetFunnelAnalysis(req, req.params.siteId, req.params.funnelId))
|
|
440
|
+
await router.post('/api/p/{siteId}/pipelines', (req: any) => funnels.handleCreateFunnel(req, req.params.siteId))
|
|
441
|
+
await router.delete('/api/p/{siteId}/pipelines/{funnelId}', (req: any) => funnels.handleDeleteFunnel(req, req.params.siteId, req.params.funnelId))
|
|
442
|
+
|
|
443
|
+
// Annotations (stealth)
|
|
444
|
+
await router.get('/api/p/{siteId}/notes', (req: any) => annotations.handleGetAnnotations(req, req.params.siteId))
|
|
445
|
+
await router.post('/api/p/{siteId}/notes', (req: any) => annotations.handleCreateAnnotation(req, req.params.siteId))
|
|
446
|
+
await router.delete('/api/p/{siteId}/notes/{annotationId}', (req: any) => annotations.handleDeleteAnnotation(req, req.params.siteId, req.params.annotationId))
|
|
447
|
+
|
|
448
|
+
// Experiments (stealth)
|
|
449
|
+
await router.get('/api/p/{siteId}/tests', (req: any) => experiments.handleGetExperiments(req, req.params.siteId))
|
|
450
|
+
await router.post('/api/p/{siteId}/tests', (req: any) => experiments.handleCreateExperiment(req, req.params.siteId))
|
|
451
|
+
await router.post('/api/p/{siteId}/tests/record', (req: any) => experiments.handleRecordExperimentEvent(req, req.params.siteId))
|
|
452
|
+
|
|
453
|
+
// Alerts (stealth)
|
|
454
|
+
await router.get('/api/p/{siteId}/notifications', (req: any) => alerts.handleGetAlerts(req, req.params.siteId))
|
|
455
|
+
await router.post('/api/p/{siteId}/notifications', (req: any) => alerts.handleCreateAlert(req, req.params.siteId))
|
|
456
|
+
await router.delete('/api/p/{siteId}/notifications/{alertId}', (req: any) => alerts.handleDeleteAlert(req, req.params.siteId, req.params.alertId))
|
|
457
|
+
|
|
458
|
+
// Email Reports (stealth)
|
|
459
|
+
await router.get('/api/p/{siteId}/scheduled', (req: any) => alerts.handleGetEmailReports(req, req.params.siteId))
|
|
460
|
+
await router.post('/api/p/{siteId}/scheduled', (req: any) => alerts.handleCreateEmailReport(req, req.params.siteId))
|
|
461
|
+
await router.delete('/api/p/{siteId}/scheduled/{reportId}', (req: any) => alerts.handleDeleteEmailReport(req, req.params.siteId, req.params.reportId))
|
|
462
|
+
|
|
463
|
+
// API Keys (stealth)
|
|
464
|
+
await router.get('/api/p/{siteId}/tokens', (req: any) => apiKeys.handleGetApiKey(req, req.params.siteId))
|
|
465
|
+
await router.post('/api/p/{siteId}/tokens/regenerate', (req: any) => apiKeys.handleRegenerateApiKey(req, req.params.siteId))
|
|
466
|
+
|
|
467
|
+
// Uptime Monitoring (stealth)
|
|
468
|
+
await router.get('/api/p/{siteId}/monitors', (req: any) => uptime.handleGetUptimeMonitors(req, req.params.siteId))
|
|
469
|
+
await router.get('/api/p/{siteId}/monitors/{monitorId}/history', (req: any) => uptime.handleGetUptimeHistory(req, req.params.siteId, req.params.monitorId))
|
|
470
|
+
await router.post('/api/p/{siteId}/monitors', (req: any) => uptime.handleCreateUptimeMonitor(req, req.params.siteId))
|
|
471
|
+
await router.delete('/api/p/{siteId}/monitors/{monitorId}', (req: any) => uptime.handleDeleteUptimeMonitor(req, req.params.siteId, req.params.monitorId))
|
|
472
|
+
|
|
473
|
+
// Webhooks (stealth)
|
|
474
|
+
await router.get('/api/p/{siteId}/hooks', (req: any) => webhooks.handleGetWebhooks(req, req.params.siteId))
|
|
475
|
+
await router.post('/api/p/{siteId}/hooks', (req: any) => webhooks.handleCreateWebhook(req, req.params.siteId))
|
|
476
|
+
await router.delete('/api/p/{siteId}/hooks/{webhookId}', (req: any) => webhooks.handleDeleteWebhook(req, req.params.siteId, req.params.webhookId))
|
|
477
|
+
|
|
478
|
+
// Team Management (stealth)
|
|
479
|
+
await router.get('/api/p/{siteId}/members', (req: any) => team.handleGetTeamMembers(req, req.params.siteId))
|
|
480
|
+
await router.post('/api/p/{siteId}/members', (req: any) => team.handleInviteTeamMember(req, req.params.siteId))
|
|
481
|
+
await router.delete('/api/p/{siteId}/members/{memberId}', (req: any) => team.handleRemoveTeamMember(req, req.params.siteId, req.params.memberId))
|
|
482
|
+
|
|
483
|
+
// Data Export & Retention (stealth)
|
|
484
|
+
await router.get('/api/p/{siteId}/download', (req: any) => data.handleExport(req, req.params.siteId))
|
|
485
|
+
await router.get('/api/p/{siteId}/storage', (req: any) => data.handleGetRetentionSettings(req, req.params.siteId))
|
|
486
|
+
await router.put('/api/p/{siteId}/storage', (req: any) => data.handleUpdateRetentionSettings(req, req.params.siteId))
|
|
487
|
+
|
|
488
|
+
// GDPR (stealth)
|
|
489
|
+
await router.get('/api/p/{siteId}/privacy/download', (req: any) => data.handleGdprExport(req, req.params.siteId))
|
|
490
|
+
await router.post('/api/p/{siteId}/privacy/remove', (req: any) => data.handleGdprDelete(req, req.params.siteId))
|
|
491
|
+
|
|
492
|
+
// Insights (stealth)
|
|
493
|
+
await router.get('/api/p/{siteId}/intel', (req: any) => data.handleGetInsights(req, req.params.siteId))
|
|
494
|
+
|
|
495
|
+
// Revenue (stealth)
|
|
496
|
+
await router.get('/api/p/{siteId}/income', (req: any) => misc.handleGetRevenue(req, req.params.siteId))
|
|
497
|
+
|
|
498
|
+
// Share Links (stealth)
|
|
499
|
+
await router.post('/api/p/{siteId}/link', (req: any) => sharing.handleCreateShareLink(req, req.params.siteId))
|
|
500
|
+
await router.get('/api/p/{siteId}/link', (req: any) => sharing.handleListShareLinks(req, req.params.siteId))
|
|
501
|
+
await router.delete('/api/p/{siteId}/link/{token}', (req: any) => sharing.handleRevokeShareLink(req, req.params.siteId, req.params.token))
|
|
502
|
+
|
|
503
|
+
// Tracking script (generates embeddable JS for site owners)
|
|
504
|
+
await router.get('/api/sites/{siteId}/script', (req: any) => {
|
|
505
|
+
return import('./handlers/views').then(v => v.handleScript(req))
|
|
506
|
+
})
|
|
507
|
+
// Raw-JS variant for the `<script src=".../script.js">` one-liner install.
|
|
508
|
+
await router.get('/api/sites/{siteId}/script.js', (req: any) => {
|
|
509
|
+
return import('./handlers/views').then(v => v.handleScript(req))
|
|
510
|
+
})
|
|
511
|
+
// Shared analytics tracker (Fathom-style): one cacheable file for all sites;
|
|
512
|
+
// the site comes from the tag's data-site attribute, the endpoint from origin.
|
|
513
|
+
await router.get('/script.js', (req: any) => {
|
|
514
|
+
return import('./handlers/views').then(v => v.handleSharedScript(req))
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
return router
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Export a singleton router instance (using top-level await)
|
|
521
|
+
export const router: Router = await createRouter()
|