Typescript client for Driftline analytics

Initial release v0.1.0

TypeScript client for Driftline Analytics.

- AnalyticsClient class for tracking events
- deriveUidFromDid helper for anonymous user ID generation
- Support for account, view, and action events

tijs.org aa8d5be3

+1
.gitignore
··· 1 + .DS_Store
+11
CHANGELOG.md
··· 1 + # Changelog 2 + 3 + ## [0.1.0] - 2025-11-28 4 + 5 + Initial release. 6 + 7 + ### Added 8 + 9 + - `AnalyticsClient` class for tracking events 10 + - `deriveUidFromDid` helper for anonymous user ID generation 11 + - Support for account, view, and action events
+80
README.md
··· 1 + # Driftline Client 2 + 3 + TypeScript client for 4 + [Driftline Analytics](https://github.com/tijs/driftline-analytics) - anonymous 5 + analytics for ATProto app views. 6 + 7 + ## Installation 8 + 9 + ```typescript 10 + // Deno / JSR 11 + import { AnalyticsClient, deriveUidFromDid } from "@tijs/driftline-client"; 12 + 13 + // Or via URL 14 + import { 15 + AnalyticsClient, 16 + deriveUidFromDid, 17 + } from "https://deno.land/x/driftline_client/mod.ts"; 18 + ``` 19 + 20 + ## Usage 21 + 22 + ```typescript 23 + import { AnalyticsClient, deriveUidFromDid } from "@tijs/driftline-client"; 24 + 25 + // Derive anonymous user ID from DID (use your app-specific salt) 26 + const uid = await deriveUidFromDid(user.did, YOUR_APP_SALT); 27 + 28 + const analytics = new AnalyticsClient({ 29 + appView: "kipclip.com", 30 + env: "prod", 31 + collectorUrl: "https://driftline.val.run", 32 + apiKey: YOUR_API_KEY, 33 + uid, 34 + }); 35 + 36 + // Track account creation (once per user) 37 + await analytics.trackAccountCreated(); 38 + 39 + // Track screen views 40 + await analytics.trackView("HomeScreen"); 41 + 42 + // Track actions 43 + await analytics.trackAction("checkin_created", "CheckinScreen", { 44 + placeType: "cafe", 45 + }); 46 + ``` 47 + 48 + ## API 49 + 50 + ### `deriveUidFromDid(did: string, salt: string): Promise<string>` 51 + 52 + Derives a pseudonymous 12-character hex user ID from a DID using SHA-256. 53 + 54 + - Same DID + salt always produces the same uid 55 + - Different salts produce different uids (for cross-app-view privacy) 56 + - Server never sees the original DID 57 + 58 + ### `AnalyticsClient` 59 + 60 + #### Constructor 61 + 62 + ```typescript 63 + new AnalyticsClient({ 64 + appView: string; // Your app view identifier 65 + env: "dev" | "prod"; // Environment 66 + collectorUrl: string; // Driftline server URL 67 + apiKey: string; // Your API key 68 + uid: string; // User ID from deriveUidFromDid 69 + }) 70 + ``` 71 + 72 + #### Methods 73 + 74 + - `trackAccountCreated(props?)` - Track account creation (once per user) 75 + - `trackView(screen, props?)` - Track screen impressions 76 + - `trackAction(name, screen?, props?)` - Track user actions 77 + 78 + ## License 79 + 80 + MIT
+20
deno.json
··· 1 + { 2 + "name": "@tijs/driftline-client", 3 + "version": "0.1.0", 4 + "exports": "./mod.ts", 5 + "compilerOptions": { 6 + "strict": true, 7 + "lib": ["dom", "dom.iterable", "deno.ns"] 8 + }, 9 + "lint": { 10 + "rules": { 11 + "exclude": ["no-explicit-any"] 12 + } 13 + }, 14 + "tasks": { 15 + "check": "deno check mod.ts", 16 + "lint": "deno lint", 17 + "fmt": "deno fmt", 18 + "test": "deno test" 19 + } 20 + }
+155
mod.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://driftline.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("[driftline] Failed to send event:", error); 92 + } 93 + } catch (err) { 94 + console.error("[driftline] 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 + }