just the currents, never the identities

Extract client library to separate repo

- Removed client/ directory (now @tijs/driftline-client)
- Removed scripts/create-api-key.ts
- Updated README to link to client repo

+6
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## [0.1.1] - 2025-11-28 4 + 5 + ### Changed 6 + 7 + - Extracted client library to separate repo (@tijs/driftline-client) 8 + 3 9 ## [0.1.0] - 2025-11-28 4 10 5 11 Initial release of Driftline Analytics.
+2 -23
README.md
··· 61 61 GET /stats/:appView/events?env=prod 62 62 ``` 63 63 64 - ## Client Usage 64 + ## Client 65 65 66 - ```typescript 67 - import { 68 - AnalyticsClient, 69 - deriveUidFromDid 70 - } from "https://esm.town/v/tijs/driftline-analytics/client/analytics-client.ts"; 71 - 72 - // Derive anonymous user ID from DID (use your app-specific salt) 73 - const uid = await deriveUidFromDid(user.did, KIPCLIP_SALT); 74 - 75 - const analytics = new AnalyticsClient({ 76 - appView: "kipclip.com", 77 - env: "prod", 78 - collectorUrl: "https://driftline.val.run", 79 - apiKey: KIPCLIP_API_KEY, 80 - uid, 81 - }); 82 - 83 - // Track events 84 - await analytics.trackAccountCreated(); 85 - await analytics.trackView("HomeScreen"); 86 - await analytics.trackAction("checkin_created", "CheckinScreen", { placeType: "cafe" }); 87 - ``` 66 + See [@tijs/driftline-client](https://jsr.io/@tijs/driftline-client) for the TypeScript client library. 88 67 89 68 ## Anonymity 90 69
-155
client/analytics-client.ts
··· 1 - /** 2 - * Driftline Analytics Client 3 - * 4 - * TypeScript client for tracking analytics events from ATProto app views. 5 - * 6 - * Usage: 7 - * const uid = await deriveUidFromDid(user.did, YOUR_APP_SALT); 8 - * const analytics = new AnalyticsClient({ 9 - * appView: "xyz.kipclip.feed", 10 - * env: "prod", 11 - * collectorUrl: "https://your-analytics.val.run", 12 - * apiKey: YOUR_API_KEY, 13 - * uid, 14 - * }); 15 - * 16 - * await analytics.trackAccountCreated(); 17 - * await analytics.trackView("HomeScreen"); 18 - * await analytics.trackAction("checkin_created", "CheckinScreen", { placeType: "cafe" }); 19 - */ 20 - 21 - export type Environment = "dev" | "prod"; 22 - export type EventType = "account" | "view" | "action"; 23 - 24 - export type AnalyticsEvent = { 25 - v: 1; 26 - appView: string; 27 - env: Environment; 28 - ts: string; 29 - uid: string; 30 - type: EventType; 31 - name: string; 32 - screen?: string; 33 - props?: Record<string, unknown>; 34 - }; 35 - 36 - export type AnalyticsClientConfig = { 37 - appView: string; 38 - env: Environment; 39 - collectorUrl: string; 40 - apiKey: string; 41 - uid: string; 42 - }; 43 - 44 - export class AnalyticsClient { 45 - constructor(private cfg: AnalyticsClientConfig) {} 46 - 47 - private createEvent( 48 - type: EventType, 49 - name: string, 50 - screen?: string, 51 - props?: Record<string, unknown>, 52 - ): AnalyticsEvent { 53 - const event: AnalyticsEvent = { 54 - v: 1, 55 - appView: this.cfg.appView, 56 - env: this.cfg.env, 57 - ts: new Date().toISOString(), 58 - uid: this.cfg.uid, 59 - type, 60 - name, 61 - }; 62 - 63 - if (screen) { 64 - event.screen = screen; 65 - } 66 - 67 - if (props && Object.keys(props).length > 0) { 68 - event.props = props; 69 - } 70 - 71 - return event; 72 - } 73 - 74 - private async send(event: AnalyticsEvent): Promise<void> { 75 - const url = this.cfg.collectorUrl.replace(/\/$/, "") + "/collect"; 76 - 77 - try { 78 - const response = await fetch(url, { 79 - method: "POST", 80 - headers: { 81 - "Content-Type": "application/json", 82 - "X-API-Key": this.cfg.apiKey, 83 - }, 84 - body: JSON.stringify(event), 85 - }); 86 - 87 - if (!response.ok) { 88 - const error = await response.json().catch(() => ({ 89 - error: "Unknown error", 90 - })); 91 - console.error("[analytics] Failed to send event:", error); 92 - } 93 - } catch (err) { 94 - console.error("[analytics] Network error:", err); 95 - } 96 - } 97 - 98 - /** 99 - * Track when an account is first created/registered for this app view. 100 - * Should only be called once per user. 101 - */ 102 - async trackAccountCreated(props?: Record<string, unknown>): Promise<void> { 103 - const event = this.createEvent( 104 - "account", 105 - "account_created", 106 - undefined, 107 - props, 108 - ); 109 - await this.send(event); 110 - } 111 - 112 - /** 113 - * Track a screen/view impression. 114 - */ 115 - async trackView( 116 - screen: string, 117 - props?: Record<string, unknown>, 118 - ): Promise<void> { 119 - const event = this.createEvent("view", "screen_impression", screen, props); 120 - await this.send(event); 121 - } 122 - 123 - /** 124 - * Track a user action. 125 - */ 126 - async trackAction( 127 - name: string, 128 - screen?: string, 129 - props?: Record<string, unknown>, 130 - ): Promise<void> { 131 - const event = this.createEvent("action", name, screen, props); 132 - await this.send(event); 133 - } 134 - } 135 - 136 - /** 137 - * Derive a pseudonymous user ID from a DID. 138 - * The same DID + salt will always produce the same uid. 139 - * Different salts (per app view) produce different uids for the same DID. 140 - * 141 - * @param did - The user's DID (e.g., "did:plc:...") 142 - * @param salt - App-specific salt (keep secret, store in env vars) 143 - * @returns 12-character hex string 144 - */ 145 - export async function deriveUidFromDid( 146 - did: string, 147 - salt: string, 148 - ): Promise<string> { 149 - const data = new TextEncoder().encode(salt + did); 150 - const hash = await crypto.subtle.digest("SHA-256", data); 151 - const hex = Array.from(new Uint8Array(hash)) 152 - .map((b) => b.toString(16).padStart(2, "0")) 153 - .join(""); 154 - return hex.slice(0, 12); 155 - }
+2 -3
deno.json
··· 1 1 { 2 2 "$schema": "https://raw.githubusercontent.com/denoland/deno/98f62cee78e85bfc47c62ed703777c6bc8794f1c/cli/schemas/config-file.v1.json", 3 3 "name": "driftline-analytics", 4 - "version": "0.1.0", 5 - "exports": "./client/analytics-client.ts", 4 + "version": "0.1.1", 6 5 "lock": false, 7 6 "compilerOptions": { 8 7 "noImplicitAny": false, ··· 26 25 "include": ["**/*.ts", "**/*.json"] 27 26 }, 28 27 "tasks": { 29 - "check": "deno check --allow-import backend/index.http.ts client/analytics-client.ts", 28 + "check": "deno check --allow-import backend/index.http.ts", 30 29 "lint": "deno lint", 31 30 "fmt": "deno fmt", 32 31 "fmt:check": "deno fmt --check",
-40
scripts/create-api-key.ts
··· 1 - /** 2 - * Script to create an API key for an app view. 3 - * Run via: deno run --allow-import scripts/create-api-key.ts <app_view> 4 - * 5 - * This script must be run in the Valtown environment (or use their API). 6 - * For local testing, copy this logic into a val and run it there. 7 - */ 8 - 9 - import { sqlite } from "https://esm.town/v/stevekrouse/sqlite?v=13"; 10 - 11 - const API_KEYS_TABLE = "driftline_api_keys"; 12 - 13 - async function createApiKey(appView: string): Promise<string> { 14 - const apiKey = crypto.randomUUID(); 15 - 16 - await sqlite.execute({ 17 - sql: 18 - `INSERT INTO ${API_KEYS_TABLE} (app_view, api_key, created) VALUES (?, ?, ?)`, 19 - args: [appView, apiKey, new Date().toISOString()], 20 - }); 21 - 22 - return apiKey; 23 - } 24 - 25 - async function main() { 26 - const appView = Deno.args[0]; 27 - 28 - if (!appView) { 29 - console.error("Usage: deno run scripts/create-api-key.ts <app_view>"); 30 - console.error("Example: deno run scripts/create-api-key.ts kipclip.com"); 31 - Deno.exit(1); 32 - } 33 - 34 - console.log(`Creating API key for app view: ${appView}`); 35 - const apiKey = await createApiKey(appView); 36 - console.log(`API key created: ${apiKey}`); 37 - console.log(`\nStore this in your Valtown secrets!`); 38 - } 39 - 40 - main();