Skip to content

Commit

Permalink
Add PostHog analytics (#124)
Browse files Browse the repository at this point in the history
* Add posthog-node to api

* Tiny fix on textarea size for AI modal

* Add enableAnalytics to the DB

* Add posthog client

* Add posthog analytics

* Remove useless log

* Add ability to toggle analytics enabled in settings

* Add README language

* Add privacy policy

* Add NODE_ENV production back to script. Refactor posthog client. Fix copy in settings and README

* Update implementation, rename DB column, fix build
  • Loading branch information
nichochar authored Jul 11, 2024
1 parent c63daba commit 95a885d
Show file tree
Hide file tree
Showing 21 changed files with 358 additions and 13 deletions.
40 changes: 40 additions & 0 deletions PRIVACY-POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Privacy Policy

## Introduction

Srcbook is committed to protecting your privacy. This policy explains what data we collect, how we collect it, how we use it, and how you can opt-out.

## Data We Collect

We collect behavioral data to understand usage patterns and improve our application. This data includes:

- Usage metrics (e.g., feature interactions, usage frequency)
- Performance metrics (e.g., load times, error rates)

## What We Do Not Collect

We do not collect any personal information or personally identifiable information (PII).

## How We Use the Data

The data collected is used to:

- Improve the functionality and performance of Srcbook
- Identify and address issues
- Understand user preferences and usage patterns

## Opt-Out Option

We respect your privacy and provide an option to opt-out of data collection. You can disable data collection at any time by going to the settings within the Srcbook application and toggling the data collection option off.

## Data Storage and Security

All collected data is stored securely and is only accessible by the development team.

## Changes to This Policy

We may update this policy from time to time. Any changes will be reflected in this document.

## Contact Us

If you have any questions about this policy, please contact us at [email protected].
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ npx srcbook
pnpm dlx srcbook
```

## Analytics and tracking

In order to improve Srcbook, we collect some behavioral analytics. We don't collect anything personal or identifiable, our goals are simply to improve the application. The code is open source so you don't have to trust us, you can verify! You can find more information in our [privacy policy](./PRIVACY-POLICY.md).

If you want to disable tracking, you can do so in the settings page of the application.

## Development

For development instructions, see [CONTRIBUTING.md](./CONTRIBUTING.md).
7 changes: 6 additions & 1 deletion packages/api/config.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { eq } from 'drizzle-orm';
import { randomid } from '@srcbook/shared';
import { configs, type Config, secrets, type Secret } from './db/schema.mjs';
import { db } from './db/index.mjs';
import { HOME_DIR } from './constants.mjs';
Expand All @@ -7,7 +8,11 @@ async function init() {
const existingConfig = await db.select().from(configs).limit(1);

if (existingConfig.length === 0) {
const defaultConfig = { baseDir: HOME_DIR, defaultLanguage: 'typescript' };
const defaultConfig = {
baseDir: HOME_DIR,
defaultLanguage: 'typescript',
installId: randomid(),
};
console.log();
console.log('Initializing application with the following configuration:\n');
console.log(JSON.stringify(defaultConfig, null, 2));
Expand Down
1 change: 1 addition & 0 deletions packages/api/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const SRCBOOK_DIR = path.join(HOME_DIR, '.srcbook');
export const SRCBOOKS_DIR = path.join(SRCBOOK_DIR, 'srcbooks');
export const DIST_DIR = _dirname;
export const PROMPTS_DIR = path.join(DIST_DIR, 'prompts');
export const IS_PRODUCTION = process.env.NODE_ENV === 'production';
8 changes: 7 additions & 1 deletion packages/api/db/schema.mts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { randomid } from '@srcbook/shared';

export const configs = sqliteTable('config', {
// Directory where .srcmd files will be stored and searched by default
// Directory where .srcmd files will be stored and searched by default.
baseDir: text('base_dir').notNull(),
defaultLanguage: text('default_language').notNull().default('typescript'),
openaiKey: text('openai_api_key'),
// Default on for behavioral analytics.
// Allows us to improve Srcbook, we don't collect any PII.
enabledAnalytics: integer('enabled_analytics', { mode: 'boolean' }).notNull().default(true),
// Stable ID for posthog
installId: text('srcbook_installation_id').notNull().default(randomid()),
});

export type Config = typeof configs.$inferSelect;
Expand Down
8 changes: 8 additions & 0 deletions packages/api/dev-server.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { WebSocketServer as WsWebSocketServer } from 'ws';
import app from './server/http.mjs';
import webSocketServer from './server/ws.mjs';

import { posthog } from './posthog-client.mjs';

export { SRCBOOK_DIR } from './constants.mjs';

const server = http.createServer(app);
Expand All @@ -15,3 +17,9 @@ const port = process.env.PORT || 2150;
server.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});

process.on('SIGINT', async function () {
await posthog.shutdown();
server.close();
process.exit();
});
2 changes: 2 additions & 0 deletions packages/api/drizzle/0003_posthog_analytics.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `config` ADD `enabled_analytics` integer DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE `config` ADD `srcbook_installation_id` text DEFAULT '18n70ookj0p2ht8c3aqfu2qjgo' NOT NULL;
102 changes: 102 additions & 0 deletions packages/api/drizzle/meta/0003_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"version": "6",
"dialect": "sqlite",
"id": "59446000-b2a1-442d-8cd1-560a6a8fa7bf",
"prevId": "fee9ddc3-ef67-45f3-a415-34fbb0b7779e",
"tables": {
"config": {
"name": "config",
"columns": {
"base_dir": {
"name": "base_dir",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_language": {
"name": "default_language",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'typescript'"
},
"openai_api_key": {
"name": "openai_api_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled_analytics": {
"name": "enabled_analytics",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"srcbook_installation_id": {
"name": "srcbook_installation_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'18n70ookj0p2ht8c3aqfu2qjgo'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"secrets": {
"name": "secrets",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"secrets_name_unique": {
"name": "secrets_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
7 changes: 7 additions & 0 deletions packages/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
"when": 1720548526434,
"tag": "0002_add-openai-key",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1720719921668,
"tag": "0003_posthog_analytics",
"breakpoints": true
}
]
}
3 changes: 2 additions & 1 deletion packages/api/index.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import app from './server/http.mjs';
import wss from './server/ws.mjs';
import { SRCBOOKS_DIR } from './constants.mjs';
import { posthog } from './posthog-client.mjs';

export { app, wss, SRCBOOKS_DIR };
export { app, wss, SRCBOOKS_DIR, posthog };
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev": "vite-node dev-server.mts",
"test": "vitest",
"prebuild": "rm -rf ./dist",
"build": "tsc && cp -R ./drizzle ./dist/drizzle && cp -R ./srcbook/examples ./dist/srcbook/examples",
"build": "tsc && cp -R ./drizzle ./dist/drizzle && cp -R ./srcbook/examples ./dist/srcbook/examples && cp -R ./prompts ./dist/prompts",
"check-types": "tsc",
"depcheck": "depcheck",
"generate": "drizzle-kit generate",
Expand All @@ -29,6 +29,7 @@
"drizzle-orm": "^0.31.2",
"express": "^4.19.2",
"marked": "^12.0.2",
"posthog-node": "^4.0.1",
"typescript": "^5.4.5",
"ws": "^8.17.0",
"zod": "^3.23.8"
Expand Down
64 changes: 64 additions & 0 deletions packages/api/posthog-client.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { PostHog } from 'posthog-node';
import { getConfig } from './config.mjs';
import { IS_PRODUCTION } from './constants.mjs';

type QueuedEvent = {
event: string;
properties?: Record<string, any>;
};

class PostHogClient {
private installId: string;
private client: PostHog | null = null;
private isEnabled: boolean = false;
private eventQueue: QueuedEvent[] = [];

constructor(config: { enabledAnalytics: boolean; installId: string }) {
this.isEnabled = config.enabledAnalytics;
this.installId = config.installId;

if (this.isEnabled) {
this.client = new PostHog(
// We're sending over API key to GitHub and clients, but it's the only way.
'phc_bQjmPYXmbl76j8gW289Qj9XILuu1STRnIfgCSKlxdgu',
{ host: 'https://us.i.posthog.com' },
);
}

this.flushQueue();
}

private flushQueue(): void {
if (!this.isEnabled || !this.client) {
this.eventQueue = []; // Clear the queue if analytics are disabled
return;
}

while (this.eventQueue.length > 0) {
const event = this.eventQueue.shift();
if (event) {
this.client.capture({ ...event, distinctId: this.installId });
}
}
}

public capture(event: QueuedEvent): void {
if (this.isEnabled && IS_PRODUCTION) {
if (this.client) {
this.client.capture({ ...event, distinctId: this.installId });
}
} else {
this.eventQueue.push(event);
}
}

public async shutdown(): Promise<void> {
this.flushQueue();
if (this.client) {
await this.client.shutdown();
}
}
}

const config = await getConfig();
export const posthog = new PostHogClient(config);
13 changes: 12 additions & 1 deletion packages/api/server/http.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Path from 'node:path';
import { posthog } from '../posthog-client.mjs';
import fs from 'node:fs/promises';
import { SRCBOOKS_DIR } from '../constants.mjs';
import express, { type Application } from 'express';
Expand Down Expand Up @@ -82,7 +83,8 @@ router.options('/srcbooks/:id', cors());
router.delete('/srcbooks/:id', cors(), async (req, res) => {
const { id } = req.params;
const srcbookDir = pathToSrcbook(id);
await removeSrcbook(srcbookDir);
removeSrcbook(srcbookDir);
posthog.capture({ event: 'user deleted srcbook' });
await deleteSessionByDirname(srcbookDir);
return res.json({ error: false, deleted: true });
});
Expand Down Expand Up @@ -117,6 +119,7 @@ router.post('/generate', cors(), async (req, res) => {
const { query } = req.body;

try {
posthog.capture({ event: 'user generated srcbook with AI', properties: { query } });
const result = await generateSrcbook(query);
const srcbookDir = await importSrcbookFromSrcmdText(result.text);
return res.json({ error: false, result: { dir: srcbookDir } });
Expand All @@ -132,6 +135,7 @@ router.options('/sessions', cors());
router.post('/sessions', cors(), async (req, res) => {
const { path } = req.body;

posthog.capture({ event: 'user opened srcbook' });
const dir = await readdir(path);

if (!dir.exists) {
Expand Down Expand Up @@ -185,6 +189,8 @@ router.post('/sessions/:id/export', cors(), async (req, res) => {

const path = Path.join(directory, filename);

posthog.capture({ event: 'user exported srcbook' });

try {
await exportSrcmdFile(session, path);
return res.json({ error: false, result: filename });
Expand Down Expand Up @@ -228,6 +234,10 @@ router.get('/settings', cors(), async (_req, res) => {
router.post('/settings', cors(), async (req, res) => {
try {
const updated = await updateConfig(req.body);
posthog.capture({
event: 'user updated settings',
properties: { setting_changed: Object.keys(req.body) },
});
return res.json({ result: updated });
} catch (e) {
const error = e as unknown as Error;
Expand All @@ -246,6 +256,7 @@ router.get('/secrets', cors(), async (_req, res) => {
// Create a new secret
router.post('/secrets', cors(), async (req, res) => {
const { name, value } = req.body;
posthog.capture({ event: 'user created secret' });
const updated = await addSecret(name, value);
return res.json({ result: updated });
});
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/generate-srcbook-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function GenerateSrcbookModal({
<Textarea
placeholder="Write a prompt to create a Srcbook..."
className="focus-visible:ring-2"
rows={4}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
Expand Down
Loading

0 comments on commit 95a885d

Please sign in to comment.