Skip to content

Commit 830102d

Browse files
committed
feat: add server side tracking support
1 parent 42113b3 commit 830102d

9 files changed

Lines changed: 169 additions & 171 deletions

File tree

manifest.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@
33
"namespace": "simple-analytics",
44
"description": "Privacy-friendly and simple Google Analytics alternative.",
55
"icon": "assets/icon.svg",
6+
"categories": ["Analytics"],
7+
"provides": ["events"],
8+
"allowCustomFields": true,
69
"permissions": {
7-
"access_client_kv": {
8-
"description": "This permission is used to facilitate better a user experience.",
9-
"required": true
10-
},
1110
"execute_unsafe_scripts": {
1211
"description": "This permission is used to facilitate better a user experience.",
1312
"required": true
1413
},
15-
"client_network_requests": {
16-
"description": "This permission is used to facilitate better a user experience.",
17-
"required": true
18-
},
1914
"server_network_requests": {
2015
"description": "This permission is used to facilitate better a user experience.",
2116
"required": true

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"author": "",
2121
"license": "Apache-2.0",
2222
"devDependencies": {
23+
"@cloudflare/workers-types": "^4.20251008.0",
2324
"@managed-components/types": "^1.3.14",
2425
"@typescript-eslint/eslint-plugin": "^7.0.2",
2526
"esbuild": "^0.20.1",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 38 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,51 @@
1-
import { ComponentSettings, Manager } from '@managed-components/types'
1+
import type { Manager } from '@managed-components/types'
2+
import { trackEvent, trackPageview } from './server/simple-analytics'
3+
import { getIncomingRequestCfProperties } from './server/utils'
4+
// import { createScript } from './script'
25

3-
interface ClientInfo {
4-
readonly emitter: string
5-
readonly userAgent: string
6-
readonly language: string
7-
readonly referer: string
8-
readonly ip: string
9-
readonly title?: string
10-
readonly timestamp?: number
11-
readonly url: URL
12-
readonly timezoneOffset?: number
13-
readonly screenWidth?: number
14-
readonly screenHeight?: number
15-
readonly viewportWidth?: number
16-
readonly viewportHeight?: number
17-
}
6+
export default async function (manager: Manager) {
7+
// manager.addEventListener('clientcreated', async event => {
8+
// const { client } = event
9+
// const script = createScript()
10+
11+
// client.execute(script)
12+
// })
1813

19-
export default async function (manager: Manager, _settings: ComponentSettings) {
20-
//
21-
manager.addEventListener('pageview', event => {
14+
manager.addEventListener('pageview', async event => {
2215
const { client } = event
16+
const { timezone } = await getIncomingRequestCfProperties()
2317

24-
const clientInfo: ClientInfo = {
25-
emitter: client.emitter,
26-
userAgent: client.userAgent,
27-
language: client.language,
28-
referer: client.referer,
29-
ip: client.ip,
30-
url: client.url,
31-
viewportHeight: client.viewportHeight,
32-
viewportWidth: client.viewportWidth,
33-
screenHeight: client.screenHeight,
34-
screenWidth: client.screenWidth,
35-
timezoneOffset: client.timezoneOffset,
36-
timestamp: client.timestamp,
37-
title: client.title,
38-
}
18+
await trackPageview({
19+
client,
20+
clientOptions: {
21+
timezone,
22+
},
23+
})
24+
})
25+
26+
manager.addEventListener('event', async event => {
27+
const { client } = event
28+
const { timezone } = await getIncomingRequestCfProperties()
3929

40-
console.log('Hello server!')
41-
event.client.execute("console.log('Hello browser')")
30+
await trackEvent('event', {
31+
client,
32+
clientOptions: {
33+
timezone,
34+
},
35+
})
4236
})
4337

44-
// historyChange
4538
manager.createEventListener('historyChange', async event => {
4639
const { client } = event
40+
const { timezone } = await getIncomingRequestCfProperties()
4741

48-
const clientInfo: ClientInfo = {
49-
emitter: client.emitter,
50-
userAgent: client.userAgent,
51-
language: client.language,
52-
referer: client.referer,
53-
ip: client.ip,
54-
url: client.url,
55-
viewportHeight: client.viewportHeight,
56-
viewportWidth: client.viewportWidth,
57-
screenHeight: client.screenHeight,
58-
screenWidth: client.screenWidth,
59-
timezoneOffset: client.timezoneOffset,
60-
timestamp: client.timestamp,
61-
title: client.title,
62-
}
42+
// client.fetch('https://queue.simpleanalyticscdn.com/noscript.gif')
6343

64-
console.info('History changed', clientInfo)
44+
await trackPageview({
45+
client,
46+
clientOptions: {
47+
timezone,
48+
},
49+
})
6550
})
6651
}

src/script.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* eslint-disable prettier/prettier */
2+
export function createScript() {
3+
return `
4+
(function () {
5+
if (document.getElementById("sa-script")) {
6+
return;
7+
}
8+
9+
const el = document.createElement("script");
10+
el.id = "sa-script";
11+
el.type = "text/javascript";
12+
el.async = true;
13+
el.src = "https://scripts.simpleanalyticscdn.com/latest.js";
14+
// TODO: set data attributes
15+
document.head.appendChild(el);
16+
})();
17+
`
18+
}

src/server/headers.ts

Lines changed: 18 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,29 @@
1-
import type { AnalyticsEvent, IgnoredMetrics } from './interfaces'
2-
3-
export function parseViewportWidth(headers: Headers) {
4-
const width =
5-
headers.get('Sec-CH-Viewport-Width') ?? headers.get('Viewport-Width')
6-
7-
return width ? Number.parseInt(width, 10) : undefined
8-
}
9-
10-
export function parseViewportHeight(headers: Headers) {
11-
const height = headers.get('Sec-CH-Viewport-Height')
12-
13-
return height ? Number.parseInt(height, 10) : undefined
14-
}
15-
16-
export function parseLanguage(headers: Headers) {
17-
const language = headers.get('Sec-CH-Lang') ?? headers.get('Lang')
18-
19-
if (!language) {
20-
const acceptLanguage = headers.get('Accept-Language')
21-
22-
return acceptLanguage ? acceptLanguage.split(',')[0] : undefined
23-
}
24-
25-
return language.split(',')[0]?.slice(0, -1)
26-
}
27-
28-
export function parseTimezone(headers: Headers) {
29-
return (
30-
headers.get('X-Vercel-IP-Timezone') ??
31-
headers.get('CloudFront-Viewer-Time-Zone') ??
32-
headers.get('Cf-Timezone') ??
33-
undefined
34-
)
35-
}
36-
37-
export function parseUserAgent(headers: Headers) {
38-
return headers.get('User-Agent') ?? ''
39-
}
40-
41-
export function parseHeaders(
42-
headers: Headers,
1+
import type {
2+
AnalyticsEvent,
3+
ClientInfo,
4+
ClientInfoOptions,
5+
IgnoredMetrics,
6+
} from './interfaces'
7+
8+
export function parseClientInfo(
9+
client: ClientInfo,
10+
options: ClientInfoOptions | undefined,
4311
ignoredMetrics: IgnoredMetrics = {}
4412
) {
4513
return {
46-
ua: !ignoredMetrics.userAgent ? parseUserAgent(headers) : '',
14+
ua: !ignoredMetrics.userAgent ? client.userAgent : '',
4715

4816
viewport_width: !ignoredMetrics.viewportSize
49-
? parseViewportWidth(headers)
17+
? client.viewportWidth
5018
: undefined,
5119
viewport_height: !ignoredMetrics.viewportSize
52-
? parseViewportHeight(headers)
20+
? client.viewportHeight
5321
: undefined,
5422

55-
language: !ignoredMetrics.language ? parseLanguage(headers) : undefined,
56-
timezone: !ignoredMetrics.timezone ? parseTimezone(headers) : undefined,
23+
screen_width: !ignoredMetrics.screenSize ? client.screenWidth : undefined,
24+
screen_height: !ignoredMetrics.screenSize ? client.screenHeight : undefined,
25+
26+
language: !ignoredMetrics.language ? client.language : undefined,
27+
timezone: !ignoredMetrics.timezone ? options?.timezone : undefined,
5728
} satisfies Partial<AnalyticsEvent>
5829
}

src/server/interfaces.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,32 @@ export interface IgnoredMetrics {
3838
userAgent?: boolean | undefined
3939
viewportSize?: boolean | undefined
4040
language?: boolean | undefined
41+
screenSize?: boolean | undefined
4142
}
4243

43-
export type ServerContext = ServerContextWithRequest | ServerContextWithPath
44-
45-
export type ServerContextWithRequest = { request: Request }
46-
47-
export type HeaderOnlyContext = { headers: Headers }
44+
export interface ClientInfo {
45+
emitter: string
46+
userAgent: string
47+
language: string
48+
referer: string
49+
ip: string
50+
url: URL
51+
title?: string | undefined
52+
timestamp?: number | undefined
53+
timezoneOffset?: number | undefined
54+
screenWidth?: number | undefined
55+
screenHeight?: number | undefined
56+
viewportWidth?: number | undefined
57+
viewportHeight?: number | undefined
58+
}
4859

49-
export type ServerContextWithPath = {
50-
path: string
51-
headers: Headers
52-
searchParams?: Record<string, string | string[] | undefined>
60+
export interface ClientInfoOptions {
61+
timezone?: string | undefined
62+
forwardedFor?: string | undefined
63+
doNotTrack?: boolean | undefined
64+
method?: string | undefined
65+
isNavigationRequest?: boolean | undefined
66+
fetchMode?: string | undefined
5367
}
5468

5569
export interface TrackingOptions {

0 commit comments

Comments
 (0)