@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,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono Integration for Analytics
|
|
3
|
+
*
|
|
4
|
+
* Middleware and route handlers for Hono framework.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { Hono } from 'hono'
|
|
9
|
+
* import { analyticsMiddleware, analyticsRoutes } from '@stacksjs/analytics/integrations/hono'
|
|
10
|
+
*
|
|
11
|
+
* const app = new Hono()
|
|
12
|
+
* app.use('*', analyticsMiddleware())
|
|
13
|
+
* app.route('/api/analytics', analyticsRoutes())
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AnalyticsConfig } from '../config'
|
|
18
|
+
import { getConfig } from '../config'
|
|
19
|
+
import {
|
|
20
|
+
generateId,
|
|
21
|
+
getDailySalt,
|
|
22
|
+
getPeriodStart,
|
|
23
|
+
hashVisitorId,
|
|
24
|
+
KeyPatterns,
|
|
25
|
+
} from '../dynamodb'
|
|
26
|
+
import type { AggregationPeriod, PageView, Session } from '../types'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
interface HonoContext {
|
|
33
|
+
req: {
|
|
34
|
+
url: string
|
|
35
|
+
method: string
|
|
36
|
+
header: (name: string) => string | undefined
|
|
37
|
+
param: (name: string) => string | undefined
|
|
38
|
+
query: (name: string) => string | undefined
|
|
39
|
+
json: <T>() => Promise<T>
|
|
40
|
+
}
|
|
41
|
+
json: (data: unknown, status?: number) => Response
|
|
42
|
+
text: (data: string, status?: number) => Response
|
|
43
|
+
body: (data: unknown, status?: number) => Response
|
|
44
|
+
set: (name: string, value: unknown) => void
|
|
45
|
+
get: (name: string) => unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type HonoMiddleware = (c: HonoContext, next: () => Promise<void>) => Promise<Response | void>
|
|
49
|
+
|
|
50
|
+
interface HonoApp {
|
|
51
|
+
use: (path: string, ...handlers: HonoMiddleware[]) => void
|
|
52
|
+
get: (path: string, handler: (c: HonoContext) => Promise<Response> | Response) => void
|
|
53
|
+
post: (path: string, handler: (c: HonoContext) => Promise<Response> | Response) => void
|
|
54
|
+
options: (path: string, handler: (c: HonoContext) => Promise<Response> | Response) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AnalyticsMiddlewareOptions {
|
|
58
|
+
/** Skip analytics for certain paths */
|
|
59
|
+
skipPaths?: string[]
|
|
60
|
+
/** Custom visitor ID generator */
|
|
61
|
+
getVisitorId?: (c: HonoContext) => Promise<string>
|
|
62
|
+
/** Store for sessions */
|
|
63
|
+
sessionStore?: SessionStore
|
|
64
|
+
/** DynamoDB command executor */
|
|
65
|
+
executeCommand: (cmd: DynamoCommand) => Promise<unknown>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SessionStore {
|
|
69
|
+
get: (key: string) => Promise<Session | null>
|
|
70
|
+
set: (key: string, session: Session, ttlSeconds?: number) => Promise<void>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface DynamoCommand {
|
|
74
|
+
command: string
|
|
75
|
+
input: Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface CollectPayload {
|
|
79
|
+
s: string // siteId
|
|
80
|
+
sid: string // sessionId
|
|
81
|
+
e: 'pageview' | 'event' | 'outbound'
|
|
82
|
+
p?: Record<string, unknown> // properties
|
|
83
|
+
u: string // url
|
|
84
|
+
r?: string // referrer
|
|
85
|
+
t?: string // title
|
|
86
|
+
sw?: number // screen width
|
|
87
|
+
sh?: number // screen height
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Middleware
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create analytics middleware for Hono
|
|
96
|
+
*
|
|
97
|
+
* Adds analytics context to requests and handles CORS.
|
|
98
|
+
*/
|
|
99
|
+
export function analyticsMiddleware(options?: Partial<AnalyticsMiddlewareOptions>): HonoMiddleware {
|
|
100
|
+
const config = getConfig()
|
|
101
|
+
|
|
102
|
+
return async (c, next) => {
|
|
103
|
+
// Add CORS headers
|
|
104
|
+
const origin = c.req.header('Origin')
|
|
105
|
+
const allowedOrigin = config.api.corsOrigins.includes('*')
|
|
106
|
+
? '*'
|
|
107
|
+
: (origin && config.api.corsOrigins.includes(origin) ? origin : config.api.corsOrigins[0])
|
|
108
|
+
|
|
109
|
+
c.set('Access-Control-Allow-Origin', allowedOrigin || '*')
|
|
110
|
+
c.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
|
111
|
+
c.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
112
|
+
|
|
113
|
+
// Handle preflight
|
|
114
|
+
if (c.req.method === 'OPTIONS') {
|
|
115
|
+
return c.body(null, 204)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Skip certain paths
|
|
119
|
+
if (options?.skipPaths) {
|
|
120
|
+
const url = new URL(c.req.url)
|
|
121
|
+
for (const skipPath of options.skipPaths) {
|
|
122
|
+
if (url.pathname.startsWith(skipPath)) {
|
|
123
|
+
return next()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add analytics context
|
|
129
|
+
c.set('analyticsConfig', config)
|
|
130
|
+
c.set('analyticsSalt', getDailySalt())
|
|
131
|
+
|
|
132
|
+
await next()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Route Handlers
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create analytics routes for Hono
|
|
142
|
+
*/
|
|
143
|
+
export function createAnalyticsRoutes(options: AnalyticsMiddlewareOptions): {
|
|
144
|
+
collect: (c: HonoContext) => Promise<Response>
|
|
145
|
+
stats: (c: HonoContext) => Promise<Response>
|
|
146
|
+
realtime: (c: HonoContext) => Promise<Response>
|
|
147
|
+
script: (c: HonoContext) => Response
|
|
148
|
+
sites: (c: HonoContext) => Promise<Response>
|
|
149
|
+
createSite: (c: HonoContext) => Promise<Response>
|
|
150
|
+
} {
|
|
151
|
+
const config = getConfig()
|
|
152
|
+
const { executeCommand, sessionStore } = options
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
/**
|
|
156
|
+
* POST /collect - Receive tracking events
|
|
157
|
+
*/
|
|
158
|
+
async collect(c) {
|
|
159
|
+
try {
|
|
160
|
+
const payload = await c.req.json<CollectPayload>()
|
|
161
|
+
|
|
162
|
+
if (!payload?.s || !payload?.e || !payload?.u) {
|
|
163
|
+
return c.json({ error: 'Missing required fields: s, e, u' }, 400)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get visitor ID
|
|
167
|
+
const ip = c.req.header('X-Forwarded-For') || c.req.header('CF-Connecting-IP') || 'unknown'
|
|
168
|
+
const userAgent = c.req.header('User-Agent') || 'unknown'
|
|
169
|
+
const salt = getDailySalt()
|
|
170
|
+
|
|
171
|
+
const visitorId = options.getVisitorId
|
|
172
|
+
? await options.getVisitorId(c)
|
|
173
|
+
: await hashVisitorId(ip, userAgent, payload.s, salt)
|
|
174
|
+
|
|
175
|
+
// Parse URL
|
|
176
|
+
let parsedUrl: URL
|
|
177
|
+
try {
|
|
178
|
+
parsedUrl = new URL(payload.u)
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return c.json({ error: 'Invalid URL' }, 400)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const timestamp = new Date()
|
|
185
|
+
const sessionId = payload.sid
|
|
186
|
+
|
|
187
|
+
// Get or create session
|
|
188
|
+
let session = sessionStore ? await sessionStore.get(`${payload.s}:${sessionId}`) : null
|
|
189
|
+
const isNewSession = !session
|
|
190
|
+
|
|
191
|
+
if (payload.e === 'pageview') {
|
|
192
|
+
const deviceInfo = parseUserAgent(userAgent)
|
|
193
|
+
const referrerSource = parseReferrerSource(payload.r)
|
|
194
|
+
|
|
195
|
+
const pageView: PageView = {
|
|
196
|
+
id: generateId(),
|
|
197
|
+
siteId: payload.s,
|
|
198
|
+
visitorId,
|
|
199
|
+
sessionId,
|
|
200
|
+
path: parsedUrl.pathname,
|
|
201
|
+
hostname: parsedUrl.hostname,
|
|
202
|
+
title: payload.t,
|
|
203
|
+
referrer: payload.r,
|
|
204
|
+
referrerSource,
|
|
205
|
+
utmSource: parsedUrl.searchParams.get('utm_source') || undefined,
|
|
206
|
+
utmMedium: parsedUrl.searchParams.get('utm_medium') || undefined,
|
|
207
|
+
utmCampaign: parsedUrl.searchParams.get('utm_campaign') || undefined,
|
|
208
|
+
deviceType: deviceInfo.deviceType,
|
|
209
|
+
browser: deviceInfo.browser,
|
|
210
|
+
os: deviceInfo.os,
|
|
211
|
+
screenWidth: payload.sw,
|
|
212
|
+
screenHeight: payload.sh,
|
|
213
|
+
isUnique: isNewSession,
|
|
214
|
+
isBounce: isNewSession,
|
|
215
|
+
timestamp,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Store page view
|
|
219
|
+
await executeCommand({
|
|
220
|
+
command: 'PutItem',
|
|
221
|
+
input: {
|
|
222
|
+
TableName: config.table.tableName,
|
|
223
|
+
Item: marshalPageView(pageView),
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Update session
|
|
228
|
+
if (session) {
|
|
229
|
+
session.pageViewCount += 1
|
|
230
|
+
session.exitPath = parsedUrl.pathname
|
|
231
|
+
session.endedAt = timestamp
|
|
232
|
+
session.isBounce = false
|
|
233
|
+
session.duration = timestamp.getTime() - session.startedAt.getTime()
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
session = {
|
|
237
|
+
id: sessionId,
|
|
238
|
+
siteId: payload.s,
|
|
239
|
+
visitorId,
|
|
240
|
+
entryPath: parsedUrl.pathname,
|
|
241
|
+
exitPath: parsedUrl.pathname,
|
|
242
|
+
referrer: payload.r,
|
|
243
|
+
referrerSource,
|
|
244
|
+
deviceType: deviceInfo.deviceType,
|
|
245
|
+
browser: deviceInfo.browser,
|
|
246
|
+
os: deviceInfo.os,
|
|
247
|
+
pageViewCount: 1,
|
|
248
|
+
eventCount: 0,
|
|
249
|
+
isBounce: true,
|
|
250
|
+
duration: 0,
|
|
251
|
+
startedAt: timestamp,
|
|
252
|
+
endedAt: timestamp,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Store session
|
|
257
|
+
await executeCommand({
|
|
258
|
+
command: 'PutItem',
|
|
259
|
+
input: {
|
|
260
|
+
TableName: config.table.tableName,
|
|
261
|
+
Item: marshalSession(session),
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
if (sessionStore) {
|
|
266
|
+
await sessionStore.set(`${payload.s}:${sessionId}`, session, 1800)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return c.body(null, 204)
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.error('Collect error:', error)
|
|
274
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* GET /sites/:siteId/stats - Get dashboard stats
|
|
280
|
+
*/
|
|
281
|
+
async stats(c) {
|
|
282
|
+
try {
|
|
283
|
+
const siteId = c.req.param('siteId')
|
|
284
|
+
const start = c.req.query('start')
|
|
285
|
+
const end = c.req.query('end')
|
|
286
|
+
|
|
287
|
+
if (!siteId) {
|
|
288
|
+
return c.json({ error: 'Missing siteId' }, 400)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const startDate = start ? new Date(start) : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
292
|
+
const endDate = end ? new Date(end) : new Date()
|
|
293
|
+
const period = determinePeriod(startDate, endDate)
|
|
294
|
+
const _periodStart = getPeriodStart(endDate, period)
|
|
295
|
+
|
|
296
|
+
const result = await executeCommand({
|
|
297
|
+
command: 'Query',
|
|
298
|
+
input: {
|
|
299
|
+
TableName: config.table.tableName,
|
|
300
|
+
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :skPrefix)',
|
|
301
|
+
ExpressionAttributeValues: {
|
|
302
|
+
':pk': { S: KeyPatterns.site.pk(siteId) },
|
|
303
|
+
':skPrefix': { S: `STATS#${period.toUpperCase()}` },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
}) as { Items?: unknown[] }
|
|
307
|
+
|
|
308
|
+
return c.json({
|
|
309
|
+
stats: result.Items || [],
|
|
310
|
+
dateRange: {
|
|
311
|
+
start: startDate.toISOString(),
|
|
312
|
+
end: endDate.toISOString(),
|
|
313
|
+
period,
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.error('Stats error:', error)
|
|
319
|
+
return c.json({ error: 'Failed to get stats' }, 500)
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* GET /sites/:siteId/realtime - Get realtime stats
|
|
325
|
+
*/
|
|
326
|
+
async realtime(c) {
|
|
327
|
+
try {
|
|
328
|
+
const siteId = c.req.param('siteId')
|
|
329
|
+
const minutes = Number.parseInt(c.req.query('minutes') || '5', 10)
|
|
330
|
+
|
|
331
|
+
if (!siteId) {
|
|
332
|
+
return c.json({ error: 'Missing siteId' }, 400)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const now = new Date()
|
|
336
|
+
const startMinute = new Date(now.getTime() - minutes * 60 * 1000)
|
|
337
|
+
|
|
338
|
+
const result = await executeCommand({
|
|
339
|
+
command: 'Query',
|
|
340
|
+
input: {
|
|
341
|
+
TableName: config.table.tableName,
|
|
342
|
+
KeyConditionExpression: 'pk = :pk AND sk BETWEEN :skStart AND :skEnd',
|
|
343
|
+
ExpressionAttributeValues: {
|
|
344
|
+
':pk': { S: KeyPatterns.site.pk(siteId) },
|
|
345
|
+
':skStart': { S: KeyPatterns.realtime.sk(startMinute.toISOString().slice(0, 16)) },
|
|
346
|
+
':skEnd': { S: KeyPatterns.realtime.sk(now.toISOString().slice(0, 16)) },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
}) as { Items?: unknown[] }
|
|
350
|
+
|
|
351
|
+
return c.json({
|
|
352
|
+
realtime: result.Items || [],
|
|
353
|
+
timestamp: now.toISOString(),
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('Realtime error:', error)
|
|
358
|
+
return c.json({ error: 'Failed to get realtime stats' }, 500)
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* GET /sites/:siteId/script - Get tracking script
|
|
364
|
+
*/
|
|
365
|
+
script(c) {
|
|
366
|
+
const siteId = c.req.param('siteId')
|
|
367
|
+
const apiEndpoint = c.req.query('api') || `${c.req.header('Host')}${config.api.basePath}`
|
|
368
|
+
|
|
369
|
+
if (!siteId) {
|
|
370
|
+
return c.json({ error: 'Missing siteId' }, 400)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const script = generateTrackingScript(siteId, apiEndpoint)
|
|
374
|
+
|
|
375
|
+
c.set('Content-Type', 'text/html')
|
|
376
|
+
c.set('Cache-Control', 'public, max-age=3600')
|
|
377
|
+
return c.text(script)
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* GET /sites - List sites
|
|
382
|
+
*/
|
|
383
|
+
async sites(c) {
|
|
384
|
+
try {
|
|
385
|
+
const ownerId = c.req.query('ownerId') || 'default'
|
|
386
|
+
|
|
387
|
+
const result = await executeCommand({
|
|
388
|
+
command: 'Query',
|
|
389
|
+
input: {
|
|
390
|
+
TableName: config.table.tableName,
|
|
391
|
+
IndexName: 'gsi1',
|
|
392
|
+
KeyConditionExpression: 'gsi1pk = :pk',
|
|
393
|
+
ExpressionAttributeValues: {
|
|
394
|
+
':pk': { S: `OWNER#${ownerId}` },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
}) as { Items?: unknown[] }
|
|
398
|
+
|
|
399
|
+
return c.json({ sites: result.Items || [] })
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
console.error('List sites error:', error)
|
|
403
|
+
return c.json({ error: 'Failed to list sites' }, 500)
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* POST /sites - Create a site
|
|
409
|
+
*/
|
|
410
|
+
async createSite(c) {
|
|
411
|
+
try {
|
|
412
|
+
const body = await c.req.json<{ name: string, domains: string[], ownerId?: string }>()
|
|
413
|
+
|
|
414
|
+
if (!body.name || !body.domains?.length) {
|
|
415
|
+
return c.json({ error: 'Missing required fields: name, domains' }, 400)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const siteId = generateId()
|
|
419
|
+
const now = new Date()
|
|
420
|
+
|
|
421
|
+
const site = {
|
|
422
|
+
id: siteId,
|
|
423
|
+
name: body.name,
|
|
424
|
+
domains: body.domains,
|
|
425
|
+
timezone: 'UTC',
|
|
426
|
+
isActive: true,
|
|
427
|
+
ownerId: body.ownerId || 'default',
|
|
428
|
+
createdAt: now.toISOString(),
|
|
429
|
+
updatedAt: now.toISOString(),
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await executeCommand({
|
|
433
|
+
command: 'PutItem',
|
|
434
|
+
input: {
|
|
435
|
+
TableName: config.table.tableName,
|
|
436
|
+
Item: {
|
|
437
|
+
pk: { S: KeyPatterns.site.pk(siteId) },
|
|
438
|
+
sk: { S: KeyPatterns.site.sk(siteId) },
|
|
439
|
+
gsi1pk: { S: KeyPatterns.site.gsi1pk(site.ownerId) },
|
|
440
|
+
gsi1sk: { S: KeyPatterns.site.gsi1sk(siteId) },
|
|
441
|
+
...marshalSite(site),
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
return c.json({ site }, 201)
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.error('Create site error:', error)
|
|
450
|
+
return c.json({ error: 'Failed to create site' }, 500)
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Mount analytics routes on a Hono app
|
|
458
|
+
*/
|
|
459
|
+
export function mountAnalyticsRoutes(app: HonoApp, basePath: string, options: AnalyticsMiddlewareOptions): void {
|
|
460
|
+
const routes = createAnalyticsRoutes(options)
|
|
461
|
+
|
|
462
|
+
app.post(`${basePath}/collect`, routes.collect)
|
|
463
|
+
app.get(`${basePath}/sites/:siteId/stats`, routes.stats)
|
|
464
|
+
app.get(`${basePath}/sites/:siteId/realtime`, routes.realtime)
|
|
465
|
+
app.get(`${basePath}/sites/:siteId/script`, routes.script)
|
|
466
|
+
app.get(`${basePath}/sites`, routes.sites)
|
|
467
|
+
app.post(`${basePath}/sites`, routes.createSite)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// Helpers
|
|
472
|
+
// ============================================================================
|
|
473
|
+
|
|
474
|
+
function determinePeriod(startDate: Date, endDate: Date): AggregationPeriod {
|
|
475
|
+
const diffMs = endDate.getTime() - startDate.getTime()
|
|
476
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24)
|
|
477
|
+
|
|
478
|
+
if (diffDays <= 2) return 'hour'
|
|
479
|
+
if (diffDays <= 90) return 'day'
|
|
480
|
+
return 'month'
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseUserAgent(ua: string): { deviceType: 'desktop' | 'mobile' | 'tablet' | 'unknown', browser: string, os: string } {
|
|
484
|
+
const deviceType = /mobile/i.test(ua) ? 'mobile' : /tablet/i.test(ua) ? 'tablet' : 'desktop'
|
|
485
|
+
|
|
486
|
+
let browser = 'Unknown'
|
|
487
|
+
if (ua.includes('Chrome')) browser = 'Chrome'
|
|
488
|
+
else if (ua.includes('Firefox')) browser = 'Firefox'
|
|
489
|
+
else if (ua.includes('Safari')) browser = 'Safari'
|
|
490
|
+
else if (ua.includes('Edge')) browser = 'Edge'
|
|
491
|
+
|
|
492
|
+
let os = 'Unknown'
|
|
493
|
+
if (ua.includes('Windows')) os = 'Windows'
|
|
494
|
+
else if (ua.includes('Mac')) os = 'macOS'
|
|
495
|
+
else if (ua.includes('Linux')) os = 'Linux'
|
|
496
|
+
else if (ua.includes('Android')) os = 'Android'
|
|
497
|
+
else if (ua.includes('iOS') || ua.includes('iPhone')) os = 'iOS'
|
|
498
|
+
|
|
499
|
+
return { deviceType, browser, os }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function parseReferrerSource(referrer?: string): string | undefined {
|
|
503
|
+
if (!referrer) return 'direct'
|
|
504
|
+
try {
|
|
505
|
+
const url = new URL(referrer)
|
|
506
|
+
const host = url.hostname.toLowerCase()
|
|
507
|
+
if (host.includes('google')) return 'google'
|
|
508
|
+
if (host.includes('bing')) return 'bing'
|
|
509
|
+
if (host.includes('twitter') || host.includes('x.com')) return 'twitter'
|
|
510
|
+
if (host.includes('facebook')) return 'facebook'
|
|
511
|
+
if (host.includes('linkedin')) return 'linkedin'
|
|
512
|
+
return host
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return undefined
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* eslint-disable prefer-const */
|
|
520
|
+
function generateTrackingScript(siteId: string, apiEndpoint: string): string {
|
|
521
|
+
return `<script>
|
|
522
|
+
(function(w,d,s,a){
|
|
523
|
+
w.analytics=w.analytics||function(){(w.analytics.q=w.analytics.q||[]).push(arguments)};
|
|
524
|
+
var f=d.getElementsByTagName(s)[0],j=d.createElement(s);
|
|
525
|
+
j.async=1;j.src='${apiEndpoint}/sites/${siteId}/script.js';
|
|
526
|
+
f.parentNode.insertBefore(j,f);
|
|
527
|
+
})(window,document,'script');
|
|
528
|
+
analytics('init','${siteId}');
|
|
529
|
+
analytics('track','pageview');
|
|
530
|
+
</script>`
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function marshalPageView(pv: PageView): Record<string, { S: string } | { N: string } | { BOOL: boolean }> {
|
|
534
|
+
return {
|
|
535
|
+
pk: { S: KeyPatterns.pageView.pk(pv.siteId) },
|
|
536
|
+
sk: { S: KeyPatterns.pageView.sk(pv.timestamp, pv.id) },
|
|
537
|
+
id: { S: pv.id },
|
|
538
|
+
siteId: { S: pv.siteId },
|
|
539
|
+
visitorId: { S: pv.visitorId },
|
|
540
|
+
sessionId: { S: pv.sessionId },
|
|
541
|
+
path: { S: pv.path },
|
|
542
|
+
hostname: { S: pv.hostname },
|
|
543
|
+
...(pv.title && { title: { S: pv.title } }),
|
|
544
|
+
...(pv.referrer && { referrer: { S: pv.referrer } }),
|
|
545
|
+
...(pv.referrerSource && { referrerSource: { S: pv.referrerSource } }),
|
|
546
|
+
...(pv.deviceType && { deviceType: { S: pv.deviceType } }),
|
|
547
|
+
...(pv.browser && { browser: { S: pv.browser } }),
|
|
548
|
+
...(pv.os && { os: { S: pv.os } }),
|
|
549
|
+
isUnique: { BOOL: pv.isUnique },
|
|
550
|
+
isBounce: { BOOL: pv.isBounce },
|
|
551
|
+
timestamp: { S: pv.timestamp.toISOString() },
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function marshalSession(s: Session): Record<string, { S: string } | { N: string } | { BOOL: boolean }> {
|
|
556
|
+
return {
|
|
557
|
+
pk: { S: KeyPatterns.session.pk(s.siteId) },
|
|
558
|
+
sk: { S: KeyPatterns.session.sk(s.id) },
|
|
559
|
+
id: { S: s.id },
|
|
560
|
+
siteId: { S: s.siteId },
|
|
561
|
+
visitorId: { S: s.visitorId },
|
|
562
|
+
entryPath: { S: s.entryPath },
|
|
563
|
+
exitPath: { S: s.exitPath },
|
|
564
|
+
...(s.referrer && { referrer: { S: s.referrer } }),
|
|
565
|
+
...(s.referrerSource && { referrerSource: { S: s.referrerSource } }),
|
|
566
|
+
...(s.deviceType && { deviceType: { S: s.deviceType } }),
|
|
567
|
+
...(s.browser && { browser: { S: s.browser } }),
|
|
568
|
+
...(s.os && { os: { S: s.os } }),
|
|
569
|
+
pageViewCount: { N: String(s.pageViewCount) },
|
|
570
|
+
eventCount: { N: String(s.eventCount) },
|
|
571
|
+
isBounce: { BOOL: s.isBounce },
|
|
572
|
+
duration: { N: String(s.duration) },
|
|
573
|
+
startedAt: { S: s.startedAt.toISOString() },
|
|
574
|
+
endedAt: { S: s.endedAt.toISOString() },
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function marshalSite(site: Record<string, unknown>): Record<string, { S: string } | { L: Array<{ S: string }> } | { BOOL: boolean }> {
|
|
579
|
+
return {
|
|
580
|
+
id: { S: site.id as string },
|
|
581
|
+
name: { S: site.name as string },
|
|
582
|
+
domains: { L: (site.domains as string[]).map(d => ({ S: d })) },
|
|
583
|
+
timezone: { S: site.timezone as string },
|
|
584
|
+
isActive: { BOOL: site.isActive as boolean },
|
|
585
|
+
ownerId: { S: site.ownerId as string },
|
|
586
|
+
createdAt: { S: site.createdAt as string },
|
|
587
|
+
updatedAt: { S: site.updatedAt as string },
|
|
588
|
+
}
|
|
589
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Integrations
|
|
3
|
+
*
|
|
4
|
+
* Pre-built integrations for popular frameworks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
analyticsMiddleware,
|
|
9
|
+
createAnalyticsRoutes,
|
|
10
|
+
mountAnalyticsRoutes,
|
|
11
|
+
type AnalyticsMiddlewareOptions,
|
|
12
|
+
} from './hono'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
createAnalyticsHandler,
|
|
16
|
+
createD1Adapter,
|
|
17
|
+
type CloudflareEnv,
|
|
18
|
+
type CloudflareHandlerOptions,
|
|
19
|
+
type StorageAdapter,
|
|
20
|
+
} from './cloudflare'
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
type StxHeadScript,
|
|
24
|
+
tsAnalytics,
|
|
25
|
+
type TsAnalyticsOptions,
|
|
26
|
+
tsAnalyticsTag,
|
|
27
|
+
} from './stx'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt Module — add ts-analytics to a Nuxt app the way you'd add `nuxt-fathom`.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // nuxt.config.ts
|
|
6
|
+
* export default defineNuxtConfig({
|
|
7
|
+
* modules: ['@stacksjs/ts-analytics/nuxt'],
|
|
8
|
+
* tsAnalytics: {
|
|
9
|
+
* appId: 'APP_ID', // that's it — the endpoint is baked in (Fathom-style)
|
|
10
|
+
* },
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Behind the scenes it injects ts-analytics' shared `/script.js` into `<head>`
|
|
15
|
+
* (keyed by your App ID via `data-site`). That tracker self-reports pageviews,
|
|
16
|
+
* **SPA route changes** (it hooks `history.pushState`/`replaceState` — i.e.
|
|
17
|
+
* vue-router/Nuxt navigation, so client-side route changes are tracked with no
|
|
18
|
+
* router wiring), link clicks, engagement, Core Web Vitals and JS errors,
|
|
19
|
+
* POSTing them to `<apiEndpoint>/collect`. It derives the collector from its own
|
|
20
|
+
* `src` origin and the App ID from `data-site`, so the only config a host app
|
|
21
|
+
* supplies is `appId` (the endpoint is baked in; override via `apiEndpoint` or
|
|
22
|
+
* the `TS_ANALYTICS_ENDPOINT` env var).
|
|
23
|
+
*
|
|
24
|
+
* For custom events, the auto-imported `useTsAnalytics()` composable wraps the
|
|
25
|
+
* tracker's global (`window.fathom`, Fathom-API compatible).
|
|
26
|
+
*
|
|
27
|
+
* Requires `@nuxt/kit` (provided by any Nuxt project; declared as an optional
|
|
28
|
+
* peer so non-Nuxt consumers of @stacksjs/ts-analytics don't pull it in).
|
|
29
|
+
*/
|
|
30
|
+
import type { Nuxt, NuxtModule } from '@nuxt/schema'
|
|
31
|
+
import { addImports, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
32
|
+
import { resolveApiEndpoint, tsAnalytics, type TsAnalyticsOptions } from './stx'
|
|
33
|
+
|
|
34
|
+
export interface TsAnalyticsNuxtOptions extends TsAnalyticsOptions {
|
|
35
|
+
/** Set false to skip injecting the tracker (e.g. in local dev). Default: true. */
|
|
36
|
+
enabled?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tsAnalyticsNuxtModule: NuxtModule<TsAnalyticsNuxtOptions> = defineNuxtModule<TsAnalyticsNuxtOptions>({
|
|
40
|
+
meta: {
|
|
41
|
+
name: '@stacksjs/ts-analytics',
|
|
42
|
+
configKey: 'tsAnalytics',
|
|
43
|
+
compatibility: { nuxt: '>=3.0.0' },
|
|
44
|
+
},
|
|
45
|
+
defaults: { enabled: true } as Partial<TsAnalyticsNuxtOptions>,
|
|
46
|
+
setup(options: TsAnalyticsNuxtOptions, nuxt: Nuxt) {
|
|
47
|
+
if (options.enabled === false)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
// tsAnalytics() returns [] when appId is missing — so a half-configured env
|
|
51
|
+
// never injects a broken tag.
|
|
52
|
+
const scripts = tsAnalytics(options)
|
|
53
|
+
if (scripts.length === 0) {
|
|
54
|
+
console.warn('[ts-analytics] module skipped — `appId` is required in nuxt.config `tsAnalytics`.')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Inject the shared tracker into <head> on every page.
|
|
59
|
+
nuxt.options.app.head.script = nuxt.options.app.head.script || []
|
|
60
|
+
nuxt.options.app.head.script.push(...(scripts as unknown[] as NonNullable<typeof nuxt.options.app.head.script>))
|
|
61
|
+
|
|
62
|
+
// Surface non-secret config for the composable / debugging.
|
|
63
|
+
;(nuxt.options.runtimeConfig.public as Record<string, unknown>).tsAnalytics = {
|
|
64
|
+
appId: options.appId,
|
|
65
|
+
apiEndpoint: resolveApiEndpoint(options.apiEndpoint),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Auto-import useTsAnalytics() for custom events.
|
|
69
|
+
const resolver = createResolver(import.meta.url)
|
|
70
|
+
addImports({
|
|
71
|
+
name: 'useTsAnalytics',
|
|
72
|
+
as: 'useTsAnalytics',
|
|
73
|
+
from: resolver.resolve('./runtime/use-ts-analytics'),
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export default tsAnalyticsNuxtModule
|