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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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 numberDiff line numberDiff line change
Expand Up@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
Comment thread
msukkari marked this conversation as resolved.
- 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 addeddocs/images/analytics_demo.mp4
Binary file not shown.
Binary file addeddocs/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 addeddocs/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 addeddocs/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 addeddocs/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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;
Comment thread
msukkari marked this conversation as resolved.

// 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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