Skip to content

feat(analytics): Adds analytics dashboard #358

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

Merged
merged 9 commits into from
Jun 20, 2025
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added analytics dashboard. [#358](https://github.com/sourcebot-dev/sourcebot/pull/358)

### Fixed
- Fixed issue where invites appeared to be created successfully, but were not actually being created in the database. [#359](https://github.com/sourcebot-dev/sourcebot/pull/359)

### Changed
- Audit logging is now enabled by default. [#358](https://github.com/sourcebot-dev/sourcebot/pull/358)

## [4.4.0] - 2025-06-18

### Added
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
]
},
"docs/features/code-navigation",
"docs/features/analytics",
"docs/features/mcp-server",
{
"group": "Agents",
Expand Down
16 changes: 8 additions & 8 deletions docs/docs/configuration/audit-logs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ action, and when the action took place.

This feature gives security and compliance teams the necessary information to ensure proper governance and administration of your Sourcebot deployment.

## Enabling Audit Logs
Audit logs must be explicitly enabled by setting the `SOURCEBOT_EE_AUDIT_LOGGING_ENABLED` [environment variable](/docs/configuration/environment-variables) to `true`
## Enabling/Disabling Audit Logs
Audit logs are enabled by default and can be controlled with the `SOURCEBOT_EE_AUDIT_LOGGING_ENABLED` [environment variable](/docs/configuration/environment-variables).

## Fetching Audit Logs
Audit logs are stored in the [postgres database](/docs/overview#architecture) connected to Sourcebot. To fetch all of the audit logs, you can use the following API:
Expand All @@ -40,7 +40,7 @@ curl --request GET '$SOURCEBOT_URL/api/ee/audit' \
{
"id": "cmc146c8r0001xgo2xyu0p463",
"timestamp": "2025-06-17T22:47:58.587Z",
"action": "query.code_search",
"action": "user.performed_code_search",
"actorId": "cmc12tnje0000xgn58jj8655h",
"actorType": "user",
"targetId": "1",
Expand All @@ -54,7 +54,7 @@ curl --request GET '$SOURCEBOT_URL/api/ee/audit' \
{
"id": "cmc12vqgb0008xgn5nv5hl9y5",
"timestamp": "2025-06-17T22:11:44.171Z",
"action": "query.code_search",
"action": "user.performed_code_search",
"actorId": "cmc12tnje0000xgn58jj8655h",
"actorType": "user",
"targetId": "1",
Expand All @@ -68,7 +68,7 @@ curl --request GET '$SOURCEBOT_URL/api/ee/audit' \
{
"id": "cmc12txwn0006xgn51ow1odid",
"timestamp": "2025-06-17T22:10:20.519Z",
"action": "query.code_search",
"action": "user.performed_code_search",
"actorId": "cmc12tnje0000xgn58jj8655h",
"actorType": "user",
"targetId": "1",
Expand Down Expand Up @@ -116,6 +116,9 @@ curl --request GET '$SOURCEBOT_URL/api/ee/audit' \
| `api_key.deleted` | `user` | `api_key` |
| `user.creation_failed` | `user` | `user` |
| `user.owner_created` | `user` | `org` |
| `user.performed_code_search` | `user` | `org` |
| `user.performed_find_references` | `user` | `org` |
| `user.performed_goto_definition` | `user` | `org` |
| `user.jit_provisioning_failed` | `user` | `org` |
| `user.jit_provisioned` | `user` | `org` |
| `user.join_request_creation_failed` | `user` | `org` |
Expand All @@ -131,9 +134,6 @@ curl --request GET '$SOURCEBOT_URL/api/ee/audit' \
| `user.signed_out` | `user` | `user` |
| `org.ownership_transfer_failed` | `user` | `org` |
| `org.ownership_transferred` | `user` | `org` |
| `query.file_source` | `user \| api_key` | `file` |
| `query.code_search` | `user \| api_key` | `org` |
| `query.list_repositories` | `user \| api_key` | `org` |


## Response schema
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The following environment variables allow you to configure your Sourcebot deploy
### Enterprise Environment Variables
| Variable | Default | Description |
| :------- | :------ | :---------- |
| `SOURCEBOT_EE_AUDIT_LOGGING_ENABLED` | `false` | <p>Enables/disables audit logging</p> |
| `SOURCEBOT_EE_AUDIT_LOGGING_ENABLED` | `true` | <p>Enables/disables audit logging</p> |
| `AUTH_EE_ENABLE_JIT_PROVISIONING` | `false` | <p>Enables/disables just-in-time user provisioning for SSO providers.</p> |
| `AUTH_EE_GITHUB_BASE_URL` | `https://github.com` | <p>The base URL for GitHub Enterprise SSO authentication.</p> |
| `AUTH_EE_GITHUB_CLIENT_ID` | `-` | <p>The client ID for GitHub Enterprise SSO authentication.</p> |
Expand Down
51 changes: 51 additions & 0 deletions docs/docs/features/analytics.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: Analytics
sidebarTitle: Analytics
---

import LicenseKeyRequired from '/snippets/license-key-required.mdx'
import { Callout } from 'nextra/components'

<LicenseKeyRequired />


## Overview

Analytics provides comprehensive insights into your organization's usage of Sourcebot, helping you understand adoption patterns and
quantify the value of time saved.

This dashboard is backed by [audit log](/docs/configuration/audit-logs) events. Please ensure you have audit logging enabled in order to see these insights.

<video
autoPlay
muted
loop
playsInline
className="w-full aspect-video"
src="/images/analytics_demo.mp4"
></video>

## Data Metrics

### Active Users
Tracks the number of unique users who performed any Sourcebot operation within each time period. This metric helps you understand team adoption
and engagement with Sourcebot.

![DAU Chart](/images/dau_chart.png)

### Code Searches
Counts the number of code search operations performed by your team.

![Code Search Chart](/images/code_search_chart.png)

### Code Navigation
Tracks "Go to Definition" and "Find All References" navigation actions. Navigation actions help developers quickly move
between code locations and understand code relationships.

![Code Nav Chart](/images/code_nav_chart.png)

## Cost Savings Calculator

The analytics dashboard includes a built-in cost savings calculator that helps you quantify the ROI of using Sourcebot.

![Cost Savings Chart](/images/cost_savings_chart.png)
Binary file added docs/images/analytics_demo.mp4
Binary file not shown.
Binary file added docs/images/code_nav_chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/code_search_chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/cost_savings_chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dau_chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateIndex
CREATE INDEX "idx_audit_core_actions_full" ON "Audit"("orgId", "timestamp", "action", "actorId");

-- CreateIndex
CREATE INDEX "idx_audit_actor_time_full" ON "Audit"("actorId", "timestamp");
6 changes: 6 additions & 0 deletions packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ model Audit {
orgId Int

@@index([actorId, actorType, targetId, targetType, orgId])

// Fast path for analytics queries – orgId is first because we assume most deployments are single tenant
@@index([orgId, timestamp, action, actorId], map: "idx_audit_core_actions_full")

// Fast path for analytics queries for a specific user
@@index([actorId, timestamp], map: "idx_audit_actor_time_full")
}

// @see : https://authjs.dev/concepts/database-models#user
Expand Down
2 changes: 2 additions & 0 deletions packages/db/tools/scriptRunner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PrismaClient } from "@sourcebot/db";
import { ArgumentParser } from "argparse";
import { migrateDuplicateConnections } from "./scripts/migrate-duplicate-connections";
import { injectAuditData } from "./scripts/inject-audit-data";
import { confirmAction } from "./utils";
import { createLogger } from "@sourcebot/logger";

Expand All @@ -10,6 +11,7 @@ export interface Script {

export const scripts: Record<string, Script> = {
"migrate-duplicate-connections": migrateDuplicateConnections,
"inject-audit-data": injectAuditData,
}

const parser = new ArgumentParser();
Expand Down
144 changes: 144 additions & 0 deletions packages/db/tools/scripts/inject-audit-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Script } from "../scriptRunner";
import { PrismaClient } from "../../dist";
import { confirmAction } from "../utils";
import { createLogger } from "@sourcebot/logger";

const logger = createLogger('inject-audit-data');

// Generate realistic audit data for analytics testing
// Simulates 50 engineers with varying activity patterns
export const injectAuditData: Script = {
run: async (prisma: PrismaClient) => {
const orgId = 1;

// Check if org exists
const org = await prisma.org.findUnique({
where: { id: orgId }
});

if (!org) {
logger.error(`Organization with id ${orgId} not found. Please create it first.`);
return;
}

logger.info(`Injecting audit data for organization: ${org.name} (${org.domain})`);

// Generate 50 fake user IDs
const userIds = Array.from({ length: 50 }, (_, i) => `user_${String(i + 1).padStart(3, '0')}`);

// Actions we're tracking
const actions = [
'user.performed_code_search',
'user.performed_find_references',
'user.performed_goto_definition'
];

// Generate data for the last 90 days
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 90);

logger.info(`Generating data from ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]}`);

confirmAction();

// Generate data for each day
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const currentDate = new Date(d);
const dayOfWeek = currentDate.getDay(); // 0 = Sunday, 6 = Saturday
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;

// For each user, generate activity for this day
for (const userId of userIds) {
// Determine if user is active today (higher chance on weekdays)
const isActiveToday = isWeekend
? Math.random() < 0.15 // 15% chance on weekends
: Math.random() < 0.85; // 85% chance on weekdays

if (!isActiveToday) continue;

// Generate code searches (2-5 per day)
const codeSearches = isWeekend
? Math.floor(Math.random() * 2) + 1 // 1-2 on weekends
: Math.floor(Math.random() * 4) + 2; // 2-5 on weekdays

// Generate navigation actions (5-10 per day)
const navigationActions = isWeekend
? Math.floor(Math.random() * 3) + 1 // 1-3 on weekends
: Math.floor(Math.random() * 6) + 5; // 5-10 on weekdays

// Create code search records
for (let i = 0; i < codeSearches; i++) {
const timestamp = new Date(currentDate);
// Spread throughout the day (9 AM to 6 PM on weekdays, more random on weekends)
if (isWeekend) {
timestamp.setHours(9 + Math.floor(Math.random() * 12));
timestamp.setMinutes(Math.floor(Math.random() * 60));
} else {
timestamp.setHours(9 + Math.floor(Math.random() * 9));
timestamp.setMinutes(Math.floor(Math.random() * 60));
}
timestamp.setSeconds(Math.floor(Math.random() * 60));

await prisma.audit.create({
data: {
timestamp,
action: 'user.performed_code_search',
actorId: userId,
actorType: 'user',
targetId: `search_${Math.floor(Math.random() * 1000)}`,
targetType: 'search',
sourcebotVersion: '1.0.0',
orgId
}
});
}

// Create navigation action records
for (let i = 0; i < navigationActions; i++) {
const timestamp = new Date(currentDate);
if (isWeekend) {
timestamp.setHours(9 + Math.floor(Math.random() * 12));
timestamp.setMinutes(Math.floor(Math.random() * 60));
} else {
timestamp.setHours(9 + Math.floor(Math.random() * 9));
timestamp.setMinutes(Math.floor(Math.random() * 60));
}
timestamp.setSeconds(Math.floor(Math.random() * 60));

// Randomly choose between find references and goto definition
const action = Math.random() < 0.6 ? 'user.performed_find_references' : 'user.performed_goto_definition';

await prisma.audit.create({
data: {
timestamp,
action,
actorId: userId,
actorType: 'user',
targetId: `symbol_${Math.floor(Math.random() * 1000)}`,
targetType: 'symbol',
sourcebotVersion: '1.0.0',
orgId
}
});
}
}
}

logger.info(`\nAudit data injection complete!`);
logger.info(`Users: ${userIds.length}`);
logger.info(`Date range: ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]}`);

// Show some statistics
const stats = await prisma.audit.groupBy({
by: ['action'],
where: { orgId },
_count: { action: true }
});

logger.info('\nAction breakdown:');
stats.forEach(stat => {
logger.info(` ${stat.action}: ${stat._count.action}`);
});
},
};
7 changes: 4 additions & 3 deletions packages/shared/src/entitlements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,16 @@ const entitlements = [
"multi-tenancy",
"sso",
"code-nav",
"audit"
"audit",
"analytics"
] as const;
export type Entitlement = (typeof entitlements)[number];

const entitlementsByPlan: Record<Plan, Entitlement[]> = {
oss: [],
"cloud:team": ["billing", "multi-tenancy", "sso", "code-nav"],
"self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit"],
"self-hosted:enterprise-unlimited": ["search-contexts", "public-access", "sso", "code-nav", "audit"],
"self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics"],
"self-hosted:enterprise-unlimited": ["search-contexts", "public-access", "sso", "code-nav", "audit", "analytics"],
// Special entitlement for https://demo.sourcebot.dev
"cloud:demo": ["public-access", "code-nav", "search-contexts"],
} as const;
Expand Down
4 changes: 3 additions & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"codemirror-lang-sparql": "^2.0.0",
"codemirror-lang-spreadsheet": "^1.3.0",
"codemirror-lang-zig": "^0.1.0",
"date-fns": "^4.1.0",
"embla-carousel-auto-scroll": "^8.3.0",
"embla-carousel-react": "^8.3.0",
"escape-string-regexp": "^5.0.0",
Expand All @@ -119,7 +120,7 @@
"graphql": "^16.9.0",
"http-status-codes": "^2.3.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.435.0",
"lucide-react": "^0.517.0",
"micromatch": "^4.0.8",
"next": "14.2.26",
"next-auth": "^5.0.0-beta.25",
Expand All @@ -138,6 +139,7 @@
"react-hotkeys-hook": "^4.5.1",
"react-icons": "^5.3.0",
"react-resizable-panels": "^2.1.1",
"recharts": "^2.15.3",
"scroll-into-view-if-needed": "^3.1.0",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
Expand Down
Loading