+1
.gitignore
+1
.gitignore
···
1
+
.DS_Store
+11
CHANGELOG.md
+11
CHANGELOG.md
+80
README.md
+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
+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
+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
+
}