Skip to content

Commit

Permalink
analytics in sdk (#1115)
Browse files Browse the repository at this point in the history
* .

* feat: capture main feature and examples

* fix: browser analytics
  • Loading branch information
louis030195 authored Jan 11, 2025
1 parent 3b32b8a commit 9de730a
Show file tree
Hide file tree
Showing 36 changed files with 1,313 additions and 218 deletions.
Binary file modified screenpipe-js/browser-sdk/bun.lockb
Binary file not shown.
5 changes: 3 additions & 2 deletions screenpipe-js/browser-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@screenpipe/browser",
"version": "0.1.15",
"version": "0.1.17",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
Expand All @@ -26,7 +26,8 @@
"@types/uuid": "^10.0.0",
"eventsource": "^3.0.2",
"express": "^4.21.1",
"node-cron": "^3.0.3"
"node-cron": "^3.0.3",
"posthog-js": "^1.205.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
Expand Down
117 changes: 96 additions & 21 deletions screenpipe-js/browser-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import type {
VisionStreamResponse,
} from "../../common/types";
import { toSnakeCase, convertToCamelCase } from "../../common/utils";
import {
captureEvent,
captureMainFeatureEvent,
identifyUser,
} from "../../common/analytics";

async function sendInputControl(action: InputAction): Promise<boolean> {
const apiUrl = "http://localhost:3030";
Expand Down Expand Up @@ -49,30 +54,63 @@ export interface BrowserPipe {
streamVision(
includeImages?: boolean
): AsyncGenerator<VisionStreamResponse, void, unknown>;
captureEvent: (
event: string,
properties?: Record<string, any>
) => Promise<void>;
captureMainFeatureEvent: (
name: string,
properties?: Record<string, any>
) => Promise<void>;
}

// Browser-only implementations
export const pipe: BrowserPipe = {
class BrowserPipeImpl implements BrowserPipe {
private analyticsInitialized = false;
private analyticsEnabled = false;
private userId?: string;
private userProperties?: Record<string, any>;

private async initAnalyticsIfNeeded() {
if (this.analyticsInitialized || !this.userId) return;

try {
const settings = { analyticsEnabled: false }; // TODO: impl settings browser side somehow ...
this.analyticsEnabled = settings.analyticsEnabled;
if (settings.analyticsEnabled) {
await identifyUser(this.userId, this.userProperties);
this.analyticsInitialized = true;
}
} catch (error) {
console.error("failed to fetch settings:", error);
}
}

async sendDesktopNotification(
options: NotificationOptions
): Promise<boolean> {
await this.initAnalyticsIfNeeded();
const notificationApiUrl = "http://localhost:11435";
try {
await fetch(`${notificationApiUrl}/notify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options),
});
await this.captureEvent("notification_sent", { success: true });
return true;
} catch (error) {
console.error("failed to send notification:", error);
await this.captureEvent("error_occurred", {
feature: "notification",
error: "send_failed",
});
return false;
}
},
}

async queryScreenpipe(
params: ScreenpipeQueryParams
): Promise<ScreenpipeResponse | null> {
await this.initAnalyticsIfNeeded();
const queryParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== "") {
Expand Down Expand Up @@ -108,21 +146,34 @@ export const pipe: BrowserPipe = {
throw new Error(`http error! status: ${response.status}`);
}
const data = await response.json();
await captureEvent("search_performed", {
content_type: params.contentType,
result_count: data.pagination.total,
});
return convertToCamelCase(data) as ScreenpipeResponse;
} catch (error) {
await captureEvent("error_occurred", {
feature: "search",
error: "query_failed",
});
console.error("error querying screenpipe:", error);
return null;
}
},
}

input: {
type: (text: string) => Promise<boolean>;
press: (key: string) => Promise<boolean>;
moveMouse: (x: number, y: number) => Promise<boolean>;
click: (button: "left" | "right" | "middle") => Promise<boolean>;
} = {
type: (text: string) => sendInputControl({ type: "WriteText", data: text }),
press: (key: string) => sendInputControl({ type: "KeyPress", data: key }),
moveMouse: (x: number, y: number) =>
sendInputControl({ type: "MouseMove", data: { x, y } }),
click: (button: "left" | "right" | "middle") =>
sendInputControl({ type: "MouseClick", data: button }),
},
};

async *streamTranscriptions(): AsyncGenerator<
TranscriptionStreamResponse,
Expand All @@ -134,6 +185,10 @@ export const pipe: BrowserPipe = {
);

try {
await this.captureEvent("stream_started", {
feature: "transcription",
});

while (true) {
const chunk: TranscriptionChunk = await new Promise(
(resolve, reject) => {
Expand Down Expand Up @@ -169,18 +224,24 @@ export const pipe: BrowserPipe = {
};
}
} finally {
await this.captureEvent("stream_ended", {
feature: "transcription",
});
eventSource.close();
}
},
}

async *streamVision(
includeImages: boolean = false
): AsyncGenerator<VisionStreamResponse, void, unknown> {
const eventSource = new EventSource(
`http://localhost:3030/sse/vision?images=${includeImages}`
);

try {
await this.captureEvent("stream_started", {
feature: "vision",
});

while (true) {
const event: VisionEvent = await new Promise((resolve, reject) => {
eventSource.onmessage = (event) => {
Expand All @@ -197,19 +258,33 @@ export const pipe: BrowserPipe = {
};
}
} finally {
await this.captureEvent("stream_ended", {
feature: "vision",
});
eventSource.close();
}
},
};

const sendDesktopNotification = pipe.sendDesktopNotification;
const queryScreenpipe = pipe.queryScreenpipe;
const input = pipe.input;

export { sendDesktopNotification, queryScreenpipe, input };
export {
toCamelCase,
toSnakeCase,
convertToCamelCase,
} from "../../common/utils";
}

public async captureEvent(
eventName: string,
properties?: Record<string, any>
) {
if (!this.analyticsEnabled) return;
await this.initAnalyticsIfNeeded();
return captureEvent(eventName, properties);
}

public async captureMainFeatureEvent(
featureName: string,
properties?: Record<string, any>
) {
if (!this.analyticsEnabled) return;
await this.initAnalyticsIfNeeded();
return captureMainFeatureEvent(featureName, properties);
}
}

const pipeImpl = new BrowserPipeImpl();
export const pipe = pipeImpl;

export * from "../../common/types";
40 changes: 40 additions & 0 deletions screenpipe-js/common/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import posthog from "posthog-js";

const POSTHOG_KEY = "phc_Bt8GoTBPgkCpDrbaIZzJIEYt0CrJjhBiuLaBck1clce";
const POSTHOG_HOST = "https://eu.i.posthog.com";

let initialized = false;

function initPosthog() {
if (!initialized) {
posthog.init(POSTHOG_KEY, { api_host: POSTHOG_HOST });
initialized = true;
}
}

export async function identifyUser(
userId: string,
properties?: Record<string, any>
): Promise<void> {
initPosthog();
posthog.identify(userId, properties);
}

export async function captureEvent(
name: string,
properties?: Record<string, any>
): Promise<void> {
initPosthog();
posthog.capture(name, properties);
}

export async function captureMainFeatureEvent(
name: string,
properties?: Record<string, any>
): Promise<void> {
initPosthog();
posthog.capture(name, {
feature: "main",
...properties,
});
}
1 change: 1 addition & 0 deletions screenpipe-js/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export interface Settings {
enableFrameCache: boolean;
enableUiMonitoring: boolean;
aiMaxContextChars: number;
analyticsEnabled: boolean;
user: User;
customSettings?: Record<string, any>;
}
Expand Down
Loading

0 comments on commit 9de730a

Please sign in to comment.