@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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A/B test statistical significance (two-proportion z-test vs the control).
|
|
3
|
+
*
|
|
4
|
+
* The first variant is treated as the control. Each other variant gets a
|
|
5
|
+
* conversion rate, uplift, two-tailed p-value, confidence, and a significance
|
|
6
|
+
* flag (p < 0.05) so the dashboard can call a winner instead of showing raw
|
|
7
|
+
* counts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Error function approximation (Abramowitz & Stegun 7.1.26). */
|
|
11
|
+
function erf(x: number): number {
|
|
12
|
+
const t = 1 / (1 + 0.3275911 * Math.abs(x))
|
|
13
|
+
const y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x)
|
|
14
|
+
return x >= 0 ? y : -y
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Standard normal CDF. */
|
|
18
|
+
function normalCdf(z: number): number {
|
|
19
|
+
return 0.5 * (1 + erf(z / Math.SQRT2))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VariantStat {
|
|
23
|
+
visitors: number
|
|
24
|
+
conversions: number
|
|
25
|
+
conversionRate: number
|
|
26
|
+
upliftPct: number | null
|
|
27
|
+
pValue: number | null
|
|
28
|
+
confidence: number | null
|
|
29
|
+
significant: boolean
|
|
30
|
+
isControl: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function analyzeVariants(variants: { visitors?: number; conversions?: number }[]): VariantStat[] {
|
|
34
|
+
if (!variants.length) return []
|
|
35
|
+
|
|
36
|
+
const control = variants[0]
|
|
37
|
+
const n1 = control.visitors || 0
|
|
38
|
+
const c1 = control.conversions || 0
|
|
39
|
+
const p1 = n1 > 0 ? c1 / n1 : 0
|
|
40
|
+
|
|
41
|
+
return variants.map((v, i) => {
|
|
42
|
+
const n2 = v.visitors || 0
|
|
43
|
+
const c2 = v.conversions || 0
|
|
44
|
+
const p2 = n2 > 0 ? c2 / n2 : 0
|
|
45
|
+
const isControl = i === 0
|
|
46
|
+
|
|
47
|
+
let upliftPct: number | null = null
|
|
48
|
+
let pValue: number | null = null
|
|
49
|
+
let confidence: number | null = null
|
|
50
|
+
let significant = false
|
|
51
|
+
|
|
52
|
+
if (!isControl && n1 > 0 && n2 > 0) {
|
|
53
|
+
upliftPct = p1 > 0 ? ((p2 - p1) / p1) * 100 : null
|
|
54
|
+
const pooled = (c1 + c2) / (n1 + n2)
|
|
55
|
+
const se = Math.sqrt(pooled * (1 - pooled) * (1 / n1 + 1 / n2))
|
|
56
|
+
if (se > 0) {
|
|
57
|
+
const z = (p2 - p1) / se
|
|
58
|
+
pValue = 2 * (1 - normalCdf(Math.abs(z)))
|
|
59
|
+
confidence = (1 - pValue) * 100
|
|
60
|
+
significant = pValue < 0.05
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { visitors: n2, conversions: c2, conversionRate: p2 * 100, upliftPct, pValue, confidence, significant, isControl }
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-site raw-data retention, honored at ingest (#178).
|
|
3
|
+
*
|
|
4
|
+
* The Settings UI has always written a RETENTION_SETTINGS item (plan-clamped),
|
|
5
|
+
* but nothing ever read it — every raw row got the global config TTL, making
|
|
6
|
+
* the setting decorative. Collect now loads the site's setting (cached
|
|
7
|
+
* in-process for 5 minutes) and every raw write stamps its TTL from it.
|
|
8
|
+
*
|
|
9
|
+
* Honest limitation: TTLs are stamped at WRITE time. Changing retention
|
|
10
|
+
* affects rows written after the change; existing rows keep their original
|
|
11
|
+
* expiry (a rewrite sweep would cost a full-partition scan per change).
|
|
12
|
+
*/
|
|
13
|
+
import { getConfig } from '../config'
|
|
14
|
+
|
|
15
|
+
const CACHE_MS = 5 * 60 * 1000
|
|
16
|
+
const cache = new Map<string, { seconds: number | null, at: number }>()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load (cached) the site's retention override in seconds, or null when the
|
|
20
|
+
* site has none configured.
|
|
21
|
+
*/
|
|
22
|
+
export async function ensureSiteRetentionLoaded(siteId: string): Promise<void> {
|
|
23
|
+
const hit = cache.get(siteId)
|
|
24
|
+
if (hit && Date.now() - hit.at < CACHE_MS)
|
|
25
|
+
return
|
|
26
|
+
try {
|
|
27
|
+
// Lazy import: the ORM imports this module for rawTtlForSite, and
|
|
28
|
+
// lib/dynamodb imports the ORM — a static import here would recreate the
|
|
29
|
+
// circular-import TDZ crash (#177).
|
|
30
|
+
const { dynamodb, TABLE_NAME, unmarshall } = await import('./dynamodb')
|
|
31
|
+
const res = await dynamodb.getItem({
|
|
32
|
+
TableName: TABLE_NAME,
|
|
33
|
+
Key: { pk: { S: `SITE#${siteId}` }, sk: { S: 'RETENTION_SETTINGS' } },
|
|
34
|
+
})
|
|
35
|
+
const days = res.Item ? Number(unmarshall(res.Item).retentionDays) : Number.NaN
|
|
36
|
+
cache.set(siteId, { seconds: Number.isFinite(days) && days > 0 ? days * 24 * 60 * 60 : null, at: Date.now() })
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Unreachable settings must never drop events — fall back to global TTL.
|
|
40
|
+
cache.set(siteId, { seconds: null, at: Date.now() })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Raw-row TTL epoch for a site: its configured retention, else global config. */
|
|
45
|
+
export function rawTtlForSite(siteId: string): number {
|
|
46
|
+
const now = Math.floor(Date.now() / 1000)
|
|
47
|
+
const override = cache.get(siteId)?.seconds
|
|
48
|
+
if (override)
|
|
49
|
+
return now + override
|
|
50
|
+
return now + getConfig().retention.rawEventTtl
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Test hook. */
|
|
54
|
+
export function clearSiteRetentionCache(): void {
|
|
55
|
+
cache.clear()
|
|
56
|
+
}
|
package/src/local.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Development Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers for running analytics locally with DynamoDB Local.
|
|
5
|
+
* Uses dynamodb-tooling for DynamoDB Local management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AnalyticsConfig } from './config'
|
|
9
|
+
import { getConfig } from './config'
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Local DynamoDB Configuration
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface LocalDynamoDBConfig {
|
|
16
|
+
/** Port for DynamoDB Local (default: 8000) */
|
|
17
|
+
port: number
|
|
18
|
+
/** Region to use (default: us-east-1) */
|
|
19
|
+
region: string
|
|
20
|
+
/** Endpoint URL */
|
|
21
|
+
endpoint: string
|
|
22
|
+
/** Whether to use shared database mode */
|
|
23
|
+
sharedDb: boolean
|
|
24
|
+
/** Path to store data (empty for in-memory) */
|
|
25
|
+
dbPath: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const defaultLocalConfig: LocalDynamoDBConfig = {
|
|
29
|
+
port: 8000,
|
|
30
|
+
region: 'us-east-1',
|
|
31
|
+
endpoint: 'http://localhost:8000',
|
|
32
|
+
sharedDb: true,
|
|
33
|
+
dbPath: '',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Table Creation
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate CreateTable input for analytics table (local development)
|
|
42
|
+
*/
|
|
43
|
+
export function generateLocalCreateTableInput(config?: AnalyticsConfig): {
|
|
44
|
+
TableName: string
|
|
45
|
+
KeySchema: Array<{ AttributeName: string, KeyType: 'HASH' | 'RANGE' }>
|
|
46
|
+
AttributeDefinitions: Array<{ AttributeName: string, AttributeType: 'S' | 'N' | 'B' }>
|
|
47
|
+
BillingMode: string
|
|
48
|
+
GlobalSecondaryIndexes?: Array<{
|
|
49
|
+
IndexName: string
|
|
50
|
+
KeySchema: Array<{ AttributeName: string, KeyType: 'HASH' | 'RANGE' }>
|
|
51
|
+
Projection: { ProjectionType: string }
|
|
52
|
+
}>
|
|
53
|
+
} {
|
|
54
|
+
const cfg = config ?? getConfig()
|
|
55
|
+
const { singleTable } = cfg.table
|
|
56
|
+
|
|
57
|
+
const input: ReturnType<typeof generateLocalCreateTableInput> = {
|
|
58
|
+
TableName: cfg.table.tableName,
|
|
59
|
+
KeySchema: [
|
|
60
|
+
{ AttributeName: singleTable.partitionKeyName, KeyType: 'HASH' },
|
|
61
|
+
{ AttributeName: singleTable.sortKeyName, KeyType: 'RANGE' },
|
|
62
|
+
],
|
|
63
|
+
AttributeDefinitions: [
|
|
64
|
+
{ AttributeName: singleTable.partitionKeyName, AttributeType: 'S' },
|
|
65
|
+
{ AttributeName: singleTable.sortKeyName, AttributeType: 'S' },
|
|
66
|
+
{ AttributeName: singleTable.gsi1pkName, AttributeType: 'S' },
|
|
67
|
+
{ AttributeName: singleTable.gsi1skName, AttributeType: 'S' },
|
|
68
|
+
{ AttributeName: singleTable.gsi2pkName, AttributeType: 'S' },
|
|
69
|
+
{ AttributeName: singleTable.gsi2skName, AttributeType: 'S' },
|
|
70
|
+
],
|
|
71
|
+
BillingMode: cfg.table.billingMode,
|
|
72
|
+
GlobalSecondaryIndexes: [
|
|
73
|
+
{
|
|
74
|
+
IndexName: 'gsi1',
|
|
75
|
+
KeySchema: [
|
|
76
|
+
{ AttributeName: singleTable.gsi1pkName, KeyType: 'HASH' },
|
|
77
|
+
{ AttributeName: singleTable.gsi1skName, KeyType: 'RANGE' },
|
|
78
|
+
],
|
|
79
|
+
Projection: { ProjectionType: 'ALL' },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
IndexName: 'gsi2',
|
|
83
|
+
KeySchema: [
|
|
84
|
+
{ AttributeName: singleTable.gsi2pkName, KeyType: 'HASH' },
|
|
85
|
+
{ AttributeName: singleTable.gsi2skName, KeyType: 'RANGE' },
|
|
86
|
+
],
|
|
87
|
+
Projection: { ProjectionType: 'ALL' },
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return input
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate AWS CLI command to create the table
|
|
97
|
+
*/
|
|
98
|
+
export function generateAwsCliCommand(config?: AnalyticsConfig): string {
|
|
99
|
+
const input = generateLocalCreateTableInput(config)
|
|
100
|
+
|
|
101
|
+
const command = [
|
|
102
|
+
'aws dynamodb create-table',
|
|
103
|
+
`--table-name ${input.TableName}`,
|
|
104
|
+
`--attribute-definitions '${JSON.stringify(input.AttributeDefinitions)}'`,
|
|
105
|
+
`--key-schema '${JSON.stringify(input.KeySchema)}'`,
|
|
106
|
+
`--billing-mode ${input.BillingMode}`,
|
|
107
|
+
`--global-secondary-indexes '${JSON.stringify(input.GlobalSecondaryIndexes)}'`,
|
|
108
|
+
'--endpoint-url http://localhost:8000',
|
|
109
|
+
'--region us-east-1',
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
return command.join(' \\\n ')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Docker Compose
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generate docker-compose.yml for local development
|
|
121
|
+
*/
|
|
122
|
+
export function generateDockerCompose(localConfig: Partial<LocalDynamoDBConfig> = {}): string {
|
|
123
|
+
const cfg = { ...defaultLocalConfig, ...localConfig }
|
|
124
|
+
|
|
125
|
+
return `version: '3.8'
|
|
126
|
+
|
|
127
|
+
services:
|
|
128
|
+
dynamodb-local:
|
|
129
|
+
image: amazon/dynamodb-local:latest
|
|
130
|
+
container_name: analytics-dynamodb
|
|
131
|
+
ports:
|
|
132
|
+
- "${cfg.port}:8000"
|
|
133
|
+
command: ["-jar", "DynamoDBLocal.jar"${cfg.sharedDb ? ', "-sharedDb"' : ''}${cfg.dbPath ? `, "-dbPath", "/data"` : ''}]
|
|
134
|
+
${cfg.dbPath
|
|
135
|
+
? `volumes:
|
|
136
|
+
- ./data/dynamodb:/data`
|
|
137
|
+
: ''}
|
|
138
|
+
networks:
|
|
139
|
+
- analytics-network
|
|
140
|
+
|
|
141
|
+
networks:
|
|
142
|
+
analytics-network:
|
|
143
|
+
driver: bridge
|
|
144
|
+
`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Seed Data
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
export interface SeedDataOptions {
|
|
152
|
+
/** Number of sites to create */
|
|
153
|
+
sites: number
|
|
154
|
+
/** Number of page views per site */
|
|
155
|
+
pageViewsPerSite: number
|
|
156
|
+
/** Number of sessions per site */
|
|
157
|
+
sessionsPerSite: number
|
|
158
|
+
/** Number of days of historical data */
|
|
159
|
+
daysOfHistory: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const defaultSeedOptions: SeedDataOptions = {
|
|
163
|
+
sites: 1,
|
|
164
|
+
pageViewsPerSite: 100,
|
|
165
|
+
sessionsPerSite: 50,
|
|
166
|
+
daysOfHistory: 7,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate seed data for local development
|
|
171
|
+
*/
|
|
172
|
+
export function generateSeedData(options: Partial<SeedDataOptions> = {}): {
|
|
173
|
+
sites: Array<Record<string, unknown>>
|
|
174
|
+
pageViews: Array<Record<string, unknown>>
|
|
175
|
+
sessions: Array<Record<string, unknown>>
|
|
176
|
+
} {
|
|
177
|
+
const opts = { ...defaultSeedOptions, ...options }
|
|
178
|
+
const sites: Array<Record<string, unknown>> = []
|
|
179
|
+
const pageViews: Array<Record<string, unknown>> = []
|
|
180
|
+
const sessions: Array<Record<string, unknown>> = []
|
|
181
|
+
|
|
182
|
+
const paths = [
|
|
183
|
+
'/',
|
|
184
|
+
'/about',
|
|
185
|
+
'/blog',
|
|
186
|
+
'/blog/post-1',
|
|
187
|
+
'/blog/post-2',
|
|
188
|
+
'/contact',
|
|
189
|
+
'/pricing',
|
|
190
|
+
'/features',
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
const referrers = [
|
|
194
|
+
{ source: 'google', url: 'https://google.com' },
|
|
195
|
+
{ source: 'twitter', url: 'https://twitter.com' },
|
|
196
|
+
{ source: 'direct', url: null },
|
|
197
|
+
{ source: 'facebook', url: 'https://facebook.com' },
|
|
198
|
+
{ source: 'linkedin', url: 'https://linkedin.com' },
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
const devices = ['desktop', 'mobile', 'tablet']
|
|
202
|
+
const browsers = ['Chrome', 'Firefox', 'Safari', 'Edge']
|
|
203
|
+
const oses = ['Windows', 'macOS', 'iOS', 'Android', 'Linux']
|
|
204
|
+
|
|
205
|
+
for (let s = 0; s < opts.sites; s++) {
|
|
206
|
+
const siteId = `site_${String(s + 1).padStart(3, '0')}`
|
|
207
|
+
|
|
208
|
+
// Create site
|
|
209
|
+
sites.push({
|
|
210
|
+
pk: `SITE#${siteId}`,
|
|
211
|
+
sk: `SITE#${siteId}`,
|
|
212
|
+
gsi1pk: `OWNER#user_001`,
|
|
213
|
+
gsi1sk: `SITE#${siteId}`,
|
|
214
|
+
id: siteId,
|
|
215
|
+
name: `Test Site ${s + 1}`,
|
|
216
|
+
domains: [`site${s + 1}.example.com`],
|
|
217
|
+
timezone: 'UTC',
|
|
218
|
+
isActive: true,
|
|
219
|
+
ownerId: 'user_001',
|
|
220
|
+
settings: JSON.stringify({
|
|
221
|
+
collectGeolocation: false,
|
|
222
|
+
trackReferrers: true,
|
|
223
|
+
trackUtmParams: true,
|
|
224
|
+
trackDeviceType: true,
|
|
225
|
+
publicDashboard: false,
|
|
226
|
+
excludedPaths: [],
|
|
227
|
+
excludedIps: [],
|
|
228
|
+
dataRetentionDays: 365,
|
|
229
|
+
}),
|
|
230
|
+
createdAt: new Date().toISOString(),
|
|
231
|
+
updatedAt: new Date().toISOString(),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Create sessions
|
|
235
|
+
for (let sess = 0; sess < opts.sessionsPerSite; sess++) {
|
|
236
|
+
const sessionId = `sess_${siteId}_${String(sess + 1).padStart(4, '0')}`
|
|
237
|
+
const visitorId = `visitor_${String(Math.floor(Math.random() * opts.sessionsPerSite / 2) + 1).padStart(4, '0')}`
|
|
238
|
+
const daysAgo = Math.floor(Math.random() * opts.daysOfHistory)
|
|
239
|
+
const startTime = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
|
|
240
|
+
const duration = Math.floor(Math.random() * 300000) // 0-5 minutes
|
|
241
|
+
const pageCount = Math.floor(Math.random() * 5) + 1
|
|
242
|
+
const referrer = referrers[Math.floor(Math.random() * referrers.length)]
|
|
243
|
+
const device = devices[Math.floor(Math.random() * devices.length)]
|
|
244
|
+
|
|
245
|
+
sessions.push({
|
|
246
|
+
pk: `SITE#${siteId}`,
|
|
247
|
+
sk: `SESSION#${sessionId}`,
|
|
248
|
+
id: sessionId,
|
|
249
|
+
siteId,
|
|
250
|
+
visitorId,
|
|
251
|
+
entryPath: paths[Math.floor(Math.random() * paths.length)],
|
|
252
|
+
exitPath: paths[Math.floor(Math.random() * paths.length)],
|
|
253
|
+
referrer: referrer.url,
|
|
254
|
+
referrerSource: referrer.source,
|
|
255
|
+
deviceType: device,
|
|
256
|
+
browser: browsers[Math.floor(Math.random() * browsers.length)],
|
|
257
|
+
os: oses[Math.floor(Math.random() * oses.length)],
|
|
258
|
+
pageViewCount: pageCount,
|
|
259
|
+
eventCount: Math.floor(Math.random() * 3),
|
|
260
|
+
isBounce: pageCount === 1,
|
|
261
|
+
duration,
|
|
262
|
+
startedAt: startTime.toISOString(),
|
|
263
|
+
endedAt: new Date(startTime.getTime() + duration).toISOString(),
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Create page views
|
|
268
|
+
for (let pv = 0; pv < opts.pageViewsPerSite; pv++) {
|
|
269
|
+
const pageViewId = `pv_${siteId}_${String(pv + 1).padStart(5, '0')}`
|
|
270
|
+
const sessionIdx = Math.floor(Math.random() * opts.sessionsPerSite)
|
|
271
|
+
const sessionId = `sess_${siteId}_${String(sessionIdx + 1).padStart(4, '0')}`
|
|
272
|
+
const visitorId = `visitor_${String(Math.floor(Math.random() * opts.sessionsPerSite / 2) + 1).padStart(4, '0')}`
|
|
273
|
+
const daysAgo = Math.floor(Math.random() * opts.daysOfHistory)
|
|
274
|
+
const timestamp = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000 - Math.random() * 24 * 60 * 60 * 1000)
|
|
275
|
+
const path = paths[Math.floor(Math.random() * paths.length)]
|
|
276
|
+
const referrer = referrers[Math.floor(Math.random() * referrers.length)]
|
|
277
|
+
|
|
278
|
+
pageViews.push({
|
|
279
|
+
pk: `SITE#${siteId}`,
|
|
280
|
+
sk: `PV#${timestamp.toISOString()}#${pageViewId}`,
|
|
281
|
+
gsi1pk: `SITE#${siteId}#DATE#${timestamp.toISOString().slice(0, 10)}`,
|
|
282
|
+
gsi1sk: `PATH#${path}#${pageViewId}`,
|
|
283
|
+
id: pageViewId,
|
|
284
|
+
siteId,
|
|
285
|
+
visitorId,
|
|
286
|
+
sessionId,
|
|
287
|
+
path,
|
|
288
|
+
hostname: `site${s + 1}.example.com`,
|
|
289
|
+
title: `Page: ${path}`,
|
|
290
|
+
referrer: referrer.url,
|
|
291
|
+
referrerSource: referrer.source,
|
|
292
|
+
deviceType: devices[Math.floor(Math.random() * devices.length)],
|
|
293
|
+
browser: browsers[Math.floor(Math.random() * browsers.length)],
|
|
294
|
+
os: oses[Math.floor(Math.random() * oses.length)],
|
|
295
|
+
screenWidth: [1920, 1440, 1366, 375, 768][Math.floor(Math.random() * 5)],
|
|
296
|
+
screenHeight: [1080, 900, 768, 667, 1024][Math.floor(Math.random() * 5)],
|
|
297
|
+
isUnique: pv % 3 === 0,
|
|
298
|
+
isBounce: false,
|
|
299
|
+
timestamp: timestamp.toISOString(),
|
|
300
|
+
ttl: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { sites, pageViews, sessions }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Setup Script
|
|
310
|
+
// ============================================================================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Print setup instructions for local development
|
|
314
|
+
*/
|
|
315
|
+
export function printLocalSetupInstructions(): void {
|
|
316
|
+
console.log(`
|
|
317
|
+
╔═══════════════════════════════════════════════════════════════════╗
|
|
318
|
+
║ Analytics Local Development Setup ║
|
|
319
|
+
╚═══════════════════════════════════════════════════════════════════╝
|
|
320
|
+
|
|
321
|
+
1. Start DynamoDB Local:
|
|
322
|
+
|
|
323
|
+
Option A: Using Docker
|
|
324
|
+
────────────────────────
|
|
325
|
+
docker run -p 8000:8000 amazon/dynamodb-local
|
|
326
|
+
|
|
327
|
+
Option B: Using docker-compose
|
|
328
|
+
────────────────────────
|
|
329
|
+
Save the docker-compose.yml and run:
|
|
330
|
+
docker-compose up -d
|
|
331
|
+
|
|
332
|
+
Option C: Using dynamodb-tooling (if installed globally)
|
|
333
|
+
────────────────────────
|
|
334
|
+
dbtooling local start
|
|
335
|
+
|
|
336
|
+
2. Create the analytics table:
|
|
337
|
+
|
|
338
|
+
${generateAwsCliCommand()}
|
|
339
|
+
|
|
340
|
+
3. Verify the table was created:
|
|
341
|
+
|
|
342
|
+
aws dynamodb list-tables --endpoint-url http://localhost:8000
|
|
343
|
+
|
|
344
|
+
4. Configure your analytics:
|
|
345
|
+
|
|
346
|
+
import { setConfig } from '@stacksjs/analytics'
|
|
347
|
+
|
|
348
|
+
setConfig({
|
|
349
|
+
table: { tableName: 'AnalyticsTable' },
|
|
350
|
+
endpoint: 'http://localhost:8000',
|
|
351
|
+
region: 'us-east-1',
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
5. Run the seed script (optional):
|
|
355
|
+
|
|
356
|
+
bun run examples/seed-local.ts
|
|
357
|
+
|
|
358
|
+
Happy developing! 🚀
|
|
359
|
+
`)
|
|
360
|
+
}
|