Skip to content

Commit e89b6b6

Browse files
tkislanclaude
andcommitted
feat(telemetry): add PostHog analytics for tracking notebook usage events
Introduces an opt-out PostHog telemetry service that tracks user interactions with Deepnote notebooks, including block additions, cell executions, environment operations, integration management, and project/notebook lifecycle events. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0ac38a0 commit e89b6b6

20 files changed

Lines changed: 524 additions & 48 deletions

package-lock.json

Lines changed: 89 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,12 @@
16501650
"description": "Disable SSL certificate verification (for development only)",
16511651
"scope": "application"
16521652
},
1653+
"deepnote.telemetry.enabled": {
1654+
"type": "boolean",
1655+
"default": true,
1656+
"description": "Enable anonymous usage telemetry to help improve Deepnote for VS Code.",
1657+
"scope": "application"
1658+
},
16531659
"deepnote.snapshots.enabled": {
16541660
"type": "boolean",
16551661
"default": true,
@@ -2725,6 +2731,7 @@
27252731
"pidtree": "^0.6.0",
27262732
"plotly.js-dist": "^3.0.1",
27272733
"portfinder": "^1.0.25",
2734+
"posthog-node": "^4.18.0",
27282735
"re-resizable": "^6.5.5",
27292736
"react": "^16.5.2",
27302737
"react-data-grid": "^6.0.2-0",

src/extension.node.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import './platform/logging';
3838
import { commands, env, ExtensionMode, UIKind, workspace, type OutputChannel } from 'vscode';
3939
import { buildApi, IExtensionApi } from './standalone/api';
4040
import { logger, setHomeDirectory } from './platform/logging';
41+
import { IPostHogAnalyticsService } from './platform/analytics/types';
4142
import { IAsyncDisposableRegistry, IExtensionContext, IsDevMode } from './platform/common/types';
4243
import { IServiceContainer, IServiceManager } from './platform/ioc/types';
4344
import { sendStartupTelemetry } from './platform/telemetry/startupTelemetry';
@@ -133,7 +134,14 @@ export function deactivate(): Thenable<void> {
133134
Exiting.isExiting = true;
134135
// Make sure to shutdown anybody who needs it.
135136
if (activatedServiceContainer) {
137+
const analytics = activatedServiceContainer.tryGet<IPostHogAnalyticsService>(IPostHogAnalyticsService);
138+
139+
if (analytics) {
140+
void analytics.shutdown();
141+
}
142+
136143
const registry = activatedServiceContainer.get<IAsyncDisposableRegistry>(IAsyncDisposableRegistry);
144+
137145
if (registry) {
138146
return registry.dispose();
139147
}

src/kernels/deepnote/environments/deepnoteEnvironmentsView.node.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { inject, injectable, named } from 'inversify';
1+
import { inject, injectable, named, optional } from 'inversify';
22
import {
33
commands,
44
Disposable,
@@ -13,6 +13,7 @@ import {
1313
import { IPythonApiProvider } from '../../../platform/api/types';
1414
import { STANDARD_OUTPUT_CHANNEL } from '../../../platform/common/constants';
1515
import { getDisplayPath } from '../../../platform/common/platform/fs-paths.node';
16+
import { IPostHogAnalyticsService } from '../../../platform/analytics/types';
1617
import { IDisposableRegistry, IOutputChannel } from '../../../platform/common/types';
1718
import { createDeepnoteServerConfigHandle } from '../../../platform/deepnote/deepnoteServerUtils.node';
1819
import { DeepnoteToolkitMissingError } from '../../../platform/errors/deepnoteKernelErrors';
@@ -52,7 +53,8 @@ export class DeepnoteEnvironmentsView implements Disposable {
5253
@inject(IDeepnoteNotebookEnvironmentMapper)
5354
private readonly notebookEnvironmentMapper: IDeepnoteNotebookEnvironmentMapper,
5455
@inject(IKernelProvider) private readonly kernelProvider: IKernelProvider,
55-
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel
56+
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel,
57+
@inject(IPostHogAnalyticsService) @optional() private readonly analytics: IPostHogAnalyticsService | undefined
5658
) {
5759
// Create tree data provider
5860

@@ -193,6 +195,11 @@ export class DeepnoteEnvironmentsView implements Disposable {
193195
const config = await this.environmentManager.createEnvironment(options, token);
194196
logger.info(`Created environment: ${config.id} (${config.name})`);
195197

198+
this.analytics?.trackEvent('create_environment', {
199+
hasDescription: !!options.description,
200+
hasPackages: !!options.packages?.length
201+
});
202+
196203
void window.showInformationMessage(
197204
l10n.t('Environment "{0}" created successfully!', config.name)
198205
);
@@ -314,6 +321,7 @@ export class DeepnoteEnvironmentsView implements Disposable {
314321
}
315322
);
316323

324+
this.analytics?.trackEvent('delete_environment');
317325
void window.showInformationMessage(l10n.t('Environment "{0}" deleted', config.name));
318326
} catch (error) {
319327
logger.error('Failed to delete environment', error);
@@ -483,6 +491,7 @@ export class DeepnoteEnvironmentsView implements Disposable {
483491
}
484492
);
485493

494+
this.analytics?.trackEvent('select_environment');
486495
void window.showInformationMessage(l10n.t('Environment switched successfully'));
487496
} catch (error) {
488497
if (error instanceof DeepnoteToolkitMissingError) {

src/kernels/deepnote/environments/deepnoteEnvironmentsView.unit.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ suite('DeepnoteEnvironmentsView', () => {
6161
instance(mockKernelAutoSelector),
6262
instance(mockNotebookEnvironmentMapper),
6363
instance(mockKernelProvider),
64-
instance(mockOutputChannel)
64+
instance(mockOutputChannel),
65+
undefined
6566
);
6667
});
6768

src/notebooks/deepnote/deepnoteActivationService.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { inject, injectable, optional } from 'inversify';
22
import { commands, l10n, workspace, window, type Disposable, type NotebookDocumentContentOptions } from 'vscode';
33

44
import { IExtensionSyncActivationService } from '../../platform/activation/types';
5+
import { IPostHogAnalyticsService } from '../../platform/analytics/types';
56
import { IExtensionContext } from '../../platform/common/types';
67
import { ILogger } from '../../platform/logging/types';
78
import { IDeepnoteNotebookManager } from '../types';
@@ -34,7 +35,8 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic
3435
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
3536
@inject(IIntegrationManager) integrationManager: IIntegrationManager,
3637
@inject(ILogger) private readonly logger: ILogger,
37-
@inject(SnapshotService) @optional() private readonly snapshotService?: SnapshotService
38+
@inject(SnapshotService) @optional() private readonly snapshotService?: SnapshotService,
39+
@inject(IPostHogAnalyticsService) @optional() private readonly analytics?: IPostHogAnalyticsService
3840
) {
3941
this.integrationManager = integrationManager;
4042
}
@@ -45,7 +47,12 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic
4547
*/
4648
public activate() {
4749
this.serializer = new DeepnoteNotebookSerializer(this.notebookManager, this.snapshotService);
48-
this.explorerView = new DeepnoteExplorerView(this.extensionContext, this.notebookManager, this.logger);
50+
this.explorerView = new DeepnoteExplorerView(
51+
this.extensionContext,
52+
this.notebookManager,
53+
this.logger,
54+
this.analytics
55+
);
4956
this.editProtection = new DeepnoteInputBlockEditProtection(this.logger);
5057
this.snapshotsEnabled = this.isSnapshotsEnabled();
5158

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { inject, injectable, optional } from 'inversify';
2+
import { Disposable } from 'vscode';
3+
4+
import { IExtensionSyncActivationService } from '../../platform/activation/types';
5+
import { IPostHogAnalyticsService } from '../../platform/analytics/types';
6+
import { IDisposableRegistry } from '../../platform/common/types';
7+
import { NotebookCellExecutionState, notebookCellExecutions } from '../../platform/notebooks/cellExecutionStateService';
8+
import { IDeepnoteNotebookManager } from '../types';
9+
10+
/**
11+
* Tracks cell execution events for PostHog analytics.
12+
*/
13+
@injectable()
14+
export class DeepnoteCellExecutionAnalytics implements IExtensionSyncActivationService {
15+
constructor(
16+
@inject(IPostHogAnalyticsService) @optional() private readonly analytics: IPostHogAnalyticsService | undefined,
17+
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
18+
@inject(IDisposableRegistry) private readonly disposables: Disposable[]
19+
) {}
20+
21+
public activate(): void {
22+
if (!this.analytics) {
23+
return;
24+
}
25+
26+
this.disposables.push(
27+
notebookCellExecutions.onDidChangeNotebookCellExecutionState((e) => {
28+
if (e.state !== NotebookCellExecutionState.Executing) {
29+
return;
30+
}
31+
32+
if (e.cell.notebook.notebookType !== 'deepnote') {
33+
return;
34+
}
35+
36+
const languageId = e.cell.document.languageId;
37+
const cellType = languageId === 'sql' ? 'sql' : languageId === 'markdown' ? 'markdown' : 'code';
38+
39+
const properties: Record<string, string> = { cellType };
40+
41+
if (cellType === 'sql') {
42+
const integrationId =
43+
e.cell.metadata?.__deepnotePocket?.sql_integration_id ?? e.cell.metadata?.sql_integration_id;
44+
45+
if (integrationId) {
46+
const projectId = e.cell.notebook.metadata?.deepnoteProjectId;
47+
48+
if (projectId) {
49+
const project = this.notebookManager.getOriginalProject(projectId);
50+
const integration = project?.project.integrations?.find((i) => i.id === integrationId);
51+
52+
if (integration?.type) {
53+
properties.integrationType = integration.type;
54+
}
55+
}
56+
}
57+
}
58+
59+
this.analytics?.trackEvent('execute_cell', properties);
60+
})
61+
);
62+
}
63+
}

0 commit comments

Comments
 (0)