Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PostHog analytics #124

Merged
merged 11 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading