Compare commits

..

12 commits

Author SHA1 Message Date
bkellam
b36e55093d Update changelog to note that lang is now case sensitive. Closes #670
Some checks are pending
Publish to ghcr / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish to ghcr / build (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
2025-12-12 14:48:50 -08:00
bkellam
e2ffda8117 fix homepage lang: example 2025-12-12 14:47:28 -08:00
Brendan Kellam
bb00a641b6
chore: Revert to using GitHub runners (#675)
* Revert "Migrate workflows to Blacksmith (#554)"

This reverts commit 0e8fdf0f97.

* Revert "use blacksmith arm machine for arm builds"

This reverts commit 595abc12be.
2025-12-12 14:36:39 -08:00
Brendan Kellam
dcb7ba66ee
fix(web): Encode parenthesis in query params (#674) 2025-12-12 13:53:30 -08:00
bkellam
58ba6e8c1a remove telemetry default placeholder from docker-compose 2025-12-12 13:23:10 -08:00
bkellam
f2b11ecd64 sourcebot v4.10.3 2025-12-12 12:44:41 -08:00
Brendan Kellam
693da36f0c
chore(web): Bump next to 15.5.9 to fix CVE-2025-55184 and CVE-2025-55183 (#673) 2025-12-12 12:42:58 -08:00
Brendan Kellam
c41087acdf
chore(web): PostHog telemetry improvements (#672) 2025-12-12 12:40:47 -08:00
msukkari
095474a901 update perm syncing docs
Some checks failed
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Has been cancelled
Update Roadmap Released / update (push) Has been cancelled
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Has been cancelled
Publish to ghcr / merge (push) Has been cancelled
2025-12-11 06:46:20 -08:00
Brendan Kellam
d63f3cf9d9
chore(web): Improve error messages for file loading errors (#665)
Some checks failed
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Has been cancelled
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Has been cancelled
Update Roadmap Released / update (push) Has been cancelled
Publish to ghcr / merge (push) Has been cancelled
2025-12-05 11:58:19 -08:00
Cade 🐀
3d85a0595c
fix: add support for anyuid to Dockerfile (#658)
Some checks are pending
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Waiting to run
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
* fix: add support for anyuid to Dockerfile

* changelog

---------

Co-authored-by: Cade Schlaefli <cade.schlaefli@mouser.com>
Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
2025-12-04 22:29:23 -08:00
Brian Phillips
84cf524d84
Add GHES support to the review agent (#611)
* add support for GHES to the review agent

* fix throttling types

---------

Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
2025-12-04 22:08:24 -08:00
32 changed files with 229 additions and 234 deletions

View file

@ -27,9 +27,9 @@ jobs:
platform: [linux/amd64, linux/arm64]
include:
- platform: linux/amd64
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
- platform: linux/arm64
runs-on: blacksmith-8vcpu-ubuntu-2204-arm
runs-on: ubuntu-24.04-arm
steps:
- name: Prepare
@ -57,8 +57,8 @@ jobs:
with:
cosign-release: "v2.2.4"
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Packages Docker Registry
uses: docker/login-action@v3
@ -69,10 +69,12 @@ jobs:
- name: Build Docker image
id: build
uses: useblacksmith/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true,annotation.org.opencontainers.image.description=Blazingly fast code search
build-args: |
@ -107,7 +109,7 @@ jobs:
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
merge:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
permissions:
packages: write
needs:
@ -120,8 +122,8 @@ jobs:
pattern: digests-*
merge-multiple: true
- name: Setup Blacksmith Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract Docker metadata
id: meta

View file

@ -8,7 +8,7 @@ on:
jobs:
build:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
permissions:
contents: read
steps:
@ -19,6 +19,6 @@ jobs:
- name: Build Docker image
id: build
uses: useblacksmith/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .

View file

@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
permissions:
contents: read
steps:

View file

@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
permissions:
contents: read
steps:

View file

@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Fixed issue where parenthesis in query params were not being encoded, resulting in a poor experience when embedding links in Markdown. [#674](https://github.com/sourcebot-dev/sourcebot/pull/674)
## [4.10.3] - 2025-12-12
### Fixed
- Fixed review agent so that it works with GHES instances [#611](https://github.com/sourcebot-dev/sourcebot/pull/611)
- Updated next package version to fix CVE-2025-55184 and CVE-2025-55183. [#673](https://github.com/sourcebot-dev/sourcebot/pull/673)
### Added
- Added support for arbitrary user IDs required for OpenShift. [#658](https://github.com/sourcebot-dev/sourcebot/pull/658)
### Updated
- Improved error messages in file source api. [#665](https://github.com/sourcebot-dev/sourcebot/pull/665)
## [4.10.2] - 2025-12-04
### Fixed
@ -41,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed the default search behaviour to match patterns as substrings and **not** regular expressions. Regular expressions can be used by toggling the regex button in search bar. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
- Renamed `public` query prefix to `visibility`. Allowed values for `visibility` are `public`, `private`, and `any`. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
- Changed `archived` query prefix to accept values `yes`, `no`, and `only`. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
- Changed `lang` query prefix to be case sensitive. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
### Removed
- Removed `case` query prefix. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)

View file

@ -195,6 +195,7 @@ RUN addgroup -g $GID sourcebot && \
adduser -D -u $UID -h /app -S sourcebot && \
adduser sourcebot postgres && \
adduser sourcebot redis && \
chown -R sourcebot /app && \
adduser sourcebot node && \
mkdir /var/log/sourcebot && \
chown sourcebot /var/log/sourcebot
@ -244,7 +245,12 @@ RUN mkdir -p /run/postgresql && \
chown -R postgres:postgres /run/postgresql && \
chmod 775 /run/postgresql
RUN chown -R sourcebot:sourcebot /data
# Make app directory accessible to both root and sourcebot user
RUN chown -R sourcebot /app \
&& chgrp -R 0 /app \
&& chmod -R g=u /app
# Make data directory accessible to both root and sourcebot user
RUN chown -R sourcebot /data
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY prefix-output.sh ./prefix-output.sh

View file

@ -22,7 +22,6 @@ services:
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres} # CHANGEME
- REDIS_URL=${REDIS_URL:-redis://redis:6379} # CHANGEME
- SOURCEBOT_EE_LICENSE_KEY=${SOURCEBOT_EE_LICENSE_KEY:-}
- SOURCEBOT_TELEMETRY_DISABLED=${SOURCEBOT_TELEMETRY_DISABLED:-false}
# For the full list of environment variables see:
# https://docs.sourcebot.dev/docs/configuration/environment-variables

View file

@ -1,14 +1,11 @@
---
title: "Permission syncing"
sidebarTitle: "Permission syncing"
tag: "experimental"
---
import LicenseKeyRequired from '/snippets/license-key-required.mdx'
import ExperimentalFeatureWarning from '/snippets/experimental-feature-warning.mdx'
<LicenseKeyRequired />
<ExperimentalFeatureWarning />
# Overview

View file

@ -147,7 +147,7 @@
"langfuse-vercel": "^3.38.4",
"lucide-react": "^0.517.0",
"micromatch": "^4.0.8",
"next": "^15.5.7",
"next": "15.5.9",
"next-auth": "^5.0.0-beta.30",
"next-navigation-guard": "^0.2.0",
"next-themes": "^0.3.0",

View file

@ -38,8 +38,8 @@ const auditService = getAuditService();
/**
* "Service Error Wrapper".
*
* Captures any thrown exceptions and converts them to a unexpected
* service error. Also logs them with Sentry.
* Captures any thrown exceptions, logs them to the console and Sentry,
* and returns a generic unexpected service error.
*/
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
try {
@ -52,10 +52,6 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
return e.serviceError;
}
if (e instanceof Error) {
return unexpectedError(e.message);
}
return unexpectedError(`An unexpected error occurred. Please try again later.`);
}
}

View file

@ -22,8 +22,12 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre
getRepoInfoByName(repoName),
]);
if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) {
return <div>Error loading file source</div>
if (isServiceError(fileSourceResponse)) {
return <div>Error loading file source: {fileSourceResponse.message}</div>
}
if (isServiceError(repoInfoResponse)) {
return <div>Error loading repo info: {repoInfoResponse.message}</div>
}
const codeHostInfo = getCodeHostInfoForRepo({

View file

@ -19,7 +19,7 @@ export const TrialIndicator = ({ subscription }: Props) => {
if (isServiceError(subscription)) {
captureEvent('wa_trial_nav_subscription_fetch_fail', {
error: subscription.errorCode,
errorCode: subscription.errorCode,
});
return null;
}

View file

@ -94,7 +94,7 @@ export const SearchLandingPage = async ({
<Query query="repo:torvalds/linux test" domain={domain}><Highlight>repo:</Highlight>torvalds/linux test</Query> <QueryExplanation>(by repo)</QueryExplanation>
</QueryExample>
<QueryExample>
<Query query="lang:typescript" domain={domain}><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation>
<Query query="lang:TypeScript" domain={domain}><Highlight>lang:</Highlight>TypeScript</Query> <QueryExplanation>(by language)</QueryExplanation>
</QueryExample>
<QueryExample>
<Query query="rev:HEAD" domain={domain}><Highlight>rev:</Highlight>HEAD</Query> <QueryExplanation>(by branch or tag)</QueryExplanation>

View file

@ -50,7 +50,7 @@ export function ChangeOrgDomainCard({ orgDomain, currentUserRole, rootDomain }:
description: `❌ Failed to update organization url. Reason: ${result.message}`,
})
captureEvent('wa_org_domain_updated_fail', {
error: result.errorCode,
errorCode: result.errorCode,
});
} else {
toast({

View file

@ -48,7 +48,7 @@ export function ChangeOrgNameCard({ orgName, currentUserRole }: ChangeOrgNameCar
description: `❌ Failed to update organization name. Reason: ${result.message}`,
})
captureEvent('wa_org_name_updated_fail', {
error: result.errorCode,
errorCode: result.errorCode,
});
} else {
toast({

View file

@ -62,7 +62,7 @@ export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvail
description: `❌ Failed to invite members. Reason: ${res.message}`
});
captureEvent('wa_invite_member_card_invite_fail', {
error: res.errorCode,
errorCode: res.errorCode,
num_emails: data.emails.length,
});
} else {

View file

@ -60,7 +60,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => {
description: `❌ Failed to cancel invite. Reason: ${response.message}`
})
captureEvent('wa_invites_list_cancel_invite_fail', {
error: response.errorCode,
errorCode: response.errorCode,
})
} else {
toast({

View file

@ -71,7 +71,7 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
description: `❌ Failed to remove member. Reason: ${response.message}`
})
captureEvent('wa_members_list_remove_member_fail', {
error: response.errorCode,
errorCode: response.errorCode,
})
} else {
toast({
@ -91,7 +91,7 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
description: `❌ Failed to transfer ownership. Reason: ${response.message}`
})
captureEvent('wa_members_list_transfer_ownership_fail', {
error: response.errorCode,
errorCode: response.errorCode,
})
} else {
toast({
@ -111,7 +111,7 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName }
description: `❌ Failed to leave organization. Reason: ${response.message}`
})
captureEvent('wa_members_list_leave_org_fail', {
error: response.errorCode,
errorCode: response.errorCode,
})
} else {
toast({

View file

@ -63,7 +63,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) =
description: `❌ Failed to approve request. Reason: ${response.message}`
})
captureEvent('wa_requests_list_approve_request_fail', {
error: response.errorCode,
errorCode: response.errorCode,
})
} else {
toast({
@ -83,7 +83,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) =
description: `❌ Failed to reject request.`
})
captureEvent('wa_requests_list_reject_request_fail', {
error: response.errorCode,
errorCode: response.errorCode,
})
} else {
toast({

View file

@ -6,28 +6,32 @@ import { WebhookEventDefinition} from "@octokit/webhooks/types";
import { EndpointDefaults } from "@octokit/types";
import { env } from "@sourcebot/shared";
import { processGitHubPullRequest } from "@/features/agents/review-agent/app";
import { throttling } from "@octokit/plugin-throttling";
import { throttling, type ThrottlingOptions } from "@octokit/plugin-throttling";
import fs from "fs";
import { GitHubPullRequest } from "@/features/agents/review-agent/types";
import { createLogger } from "@sourcebot/shared";
const logger = createLogger('github-webhook');
let githubApp: App | undefined;
const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
type GitHubAppBaseOptions = Omit<ConstructorParameters<typeof App>[0], "Octokit"> & { throttle: ThrottlingOptions };
let githubAppBaseOptions: GitHubAppBaseOptions | undefined;
const githubAppCache = new Map<string, App>();
if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET && env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH) {
try {
const privateKey = fs.readFileSync(env.GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH, "utf8");
const throttledOctokit = Octokit.plugin(throttling);
githubApp = new App({
githubAppBaseOptions = {
appId: env.GITHUB_REVIEW_AGENT_APP_ID,
privateKey: privateKey,
privateKey,
webhooks: {
secret: env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET,
},
Octokit: throttledOctokit,
throttle: {
onRateLimit: (retryAfter: number, options: Required<EndpointDefaults>, octokit: Octokit, retryCount: number) => {
enabled: true,
onRateLimit: (retryAfter, _options, _octokit, retryCount) => {
if (retryCount > 3) {
logger.warn(`Rate limit exceeded: ${retryAfter} seconds`);
return false;
@ -35,13 +39,55 @@ if (env.GITHUB_REVIEW_AGENT_APP_ID && env.GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET
return true;
},
}
});
onSecondaryRateLimit: (_retryAfter, options) => {
// no retries on secondary rate limits
logger.warn(`SecondaryRateLimit detected for ${options.method} ${options.url}`);
}
},
};
} catch (error) {
logger.error(`Error initializing GitHub app: ${error}`);
}
}
const normalizeGithubApiBaseUrl = (baseUrl?: string) => {
if (!baseUrl) {
return DEFAULT_GITHUB_API_BASE_URL;
}
return baseUrl.replace(/\/+$/, "");
};
const resolveGithubApiBaseUrl = (headers: Record<string, string>) => {
const enterpriseHost = headers["x-github-enterprise-host"];
if (enterpriseHost) {
return normalizeGithubApiBaseUrl(`https://${enterpriseHost}/api/v3`);
}
return DEFAULT_GITHUB_API_BASE_URL;
};
const getGithubAppForBaseUrl = (baseUrl: string) => {
if (!githubAppBaseOptions) {
return undefined;
}
const normalizedBaseUrl = normalizeGithubApiBaseUrl(baseUrl);
const cachedApp = githubAppCache.get(normalizedBaseUrl);
if (cachedApp) {
return cachedApp;
}
const OctokitWithBaseUrl = Octokit.plugin(throttling).defaults({ baseUrl: normalizedBaseUrl });
const app = new App({
...githubAppBaseOptions,
Octokit: OctokitWithBaseUrl,
});
githubAppCache.set(normalizedBaseUrl, app);
return app;
};
function isPullRequestEvent(eventHeader: string, payload: unknown): payload is WebhookEventDefinition<"pull-request-opened"> | WebhookEventDefinition<"pull-request-synchronize"> {
return eventHeader === "pull_request" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && (payload.action === "opened" || payload.action === "synchronize");
}
@ -52,12 +98,16 @@ function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is
export const POST = async (request: NextRequest) => {
const body = await request.json();
const headers = Object.fromEntries(request.headers.entries());
const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value]));
const githubEvent = headers['x-github-event'] || headers['X-GitHub-Event'];
const githubEvent = headers['x-github-event'];
if (githubEvent) {
logger.info('GitHub event received:', githubEvent);
const githubApiBaseUrl = resolveGithubApiBaseUrl(headers);
logger.debug('Using GitHub API base URL for event', { githubApiBaseUrl });
const githubApp = getGithubAppForBaseUrl(githubApiBaseUrl);
if (!githubApp) {
logger.warn('Received GitHub webhook event but GitHub app env vars are not set');
return Response.json({ status: 'ok' });
@ -113,4 +163,4 @@ export const POST = async (request: NextRequest) => {
}
return Response.json({ status: 'ok' });
}
}

View file

@ -7,6 +7,7 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { SessionProvider } from "next-auth/react";
import { env } from "@sourcebot/shared";
import { env as clientEnv } from "@sourcebot/shared/client";
import { PlanProvider } from "@/features/entitlements/planProvider";
import { getEntitlements } from "@sourcebot/shared";
@ -42,6 +43,8 @@ export default function RootLayout({
// @note: the posthog api key doesn't need to be kept secret,
// so we are safe to send it to the client.
posthogApiKey={env.POSTHOG_PAPIK}
sourcebotVersion={clientEnv.NEXT_PUBLIC_SOURCEBOT_VERSION}
sourcebotInstallId={env.SOURCEBOT_INSTALL_ID}
>
<ThemeProvider
attribute="class"

View file

@ -34,9 +34,17 @@ interface PostHogProviderProps {
children: React.ReactNode
isDisabled: boolean
posthogApiKey: string
sourcebotVersion: string
sourcebotInstallId: string
}
export function PostHogProvider({ children, isDisabled, posthogApiKey }: PostHogProviderProps) {
export function PostHogProvider({
children,
isDisabled,
posthogApiKey,
sourcebotVersion,
sourcebotInstallId,
}: PostHogProviderProps) {
const { data: session } = useSession();
useEffect(() => {
@ -61,27 +69,33 @@ export function PostHogProvider({ children, isDisabled, posthogApiKey }: PostHog
'$referrer',
'$referring_domain',
'$ip',
] : []
] : [],
loaded: (posthog) => {
// Include install id & version in all events.
posthog.register({
sourcebot_version: sourcebotVersion,
install_id: sourcebotInstallId,
});
}
});
} else {
console.debug("PostHog telemetry disabled");
}
}, [isDisabled, posthogApiKey]);
}, [isDisabled, posthogApiKey, sourcebotInstallId, sourcebotVersion]);
useEffect(() => {
if (!session) {
return;
}
// Only identify the user if we are running in a cloud environment.
if (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined) {
posthog.identify(session.user.id, {
posthog.identify(
session.user.id,
// Only include email & name when running in a cloud environment.
env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined ? {
email: session.user.email,
name: session.user.name,
});
} else {
console.debug("PostHog identify skipped");
}
} : undefined
);
}, [session]);
return (

View file

@ -55,7 +55,7 @@ export function ChangeBillingEmailCard({ currentUserRole, billingEmail }: Change
description: "❌ Failed to update billing email. Please try again.",
})
captureEvent('wa_billing_email_updated_fail', {
error: result.message,
errorCode: result.errorCode,
})
}
setIsLoading(false)

View file

@ -30,7 +30,7 @@ export const Checkout = () => {
variant: "destructive",
});
captureEvent('wa_onboard_checkout_fail', {
error: errorMessage,
errorCode: errorMessage,
});
}
}, [errorCode, errorMessage, toast, captureEvent]);
@ -45,7 +45,7 @@ export const Checkout = () => {
variant: "destructive",
})
captureEvent('wa_onboard_checkout_fail', {
error: response.errorCode,
errorCode: response.errorCode,
});
} else {
captureEvent('wa_onboard_checkout_success', {});

View file

@ -21,7 +21,7 @@ export function ManageSubscriptionButton({ currentUserRole }: { currentUserRole:
const session = await getCustomerPortalSessionLink(domain);
if (isServiceError(session)) {
captureEvent('wa_manage_subscription_button_create_portal_session_fail', {
error: session.errorCode,
errorCode: session.errorCode,
});
setIsLoading(false);
} else {

View file

@ -32,7 +32,7 @@ export const TeamUpgradeCard = ({ buttonText }: TeamUpgradeCardProps) => {
variant: "destructive",
});
captureEvent('wa_team_upgrade_checkout_fail', {
error: response.errorCode,
errorCode: response.errorCode,
});
} else {
router.push(response.url);

View file

@ -3,17 +3,13 @@
import { CaptureOptions } from "posthog-js";
import posthog from "posthog-js";
import { PosthogEvent, PosthogEventMap } from "../lib/posthogEvents";
import { env } from "@sourcebot/shared/client";
export function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E], options?: CaptureOptions) {
if(!options) {
options = {};
}
options.send_instantly = true;
posthog.capture(event, {
...properties,
sourcebot_version: env.NEXT_PUBLIC_SOURCEBOT_VERSION,
}, options);
posthog.capture(event, properties, options);
}
/**

View file

@ -1,9 +1,12 @@
import { PostHog } from 'posthog-node'
import { env } from '@sourcebot/shared'
import { env as clientEnv } from '@sourcebot/shared/client';
import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
import * as Sentry from "@sentry/nextjs";
import { PosthogEvent, PosthogEventMap } from './posthogEvents';
import { cookies } from 'next/headers';
import { cookies, headers } from 'next/headers';
import { auth } from '@/auth';
import { getVerifiedApiObject } from '@/withAuthV2';
/**
* @note: This is a subset of the properties stored in the
@ -47,13 +50,43 @@ const getPostHogCookie = (cookieStore: Pick<RequestCookies, 'get'>): PostHogCook
return undefined;
}
/**
* Attempts to retrieve the distinct id of the current user.
*/
const tryGetDistinctId = async () => {
// First, attempt to retrieve the distinct id from the cookie.
const cookieStore = await cookies();
const cookie = getPostHogCookie(cookieStore);
if (cookie) {
return cookie.distinct_id;
}
// Next, from the session.
const session = await auth();
if (session) {
return session.user.id;
}
// Finally, from the api key.
const headersList = await headers();
const apiKeyString = headersList.get("X-Sourcebot-Api-Key") ?? undefined;
if (!apiKeyString) {
return undefined;
}
const apiKey = await getVerifiedApiObject(apiKeyString);
return apiKey?.createdById;
}
export async function captureEvent<E extends PosthogEvent>(event: E, properties: PosthogEventMap[E]) {
if (env.SOURCEBOT_TELEMETRY_DISABLED === 'true') {
return;
}
const cookieStore = await cookies();
const cookie = getPostHogCookie(cookieStore);
const distinctId = await tryGetDistinctId();
const headersList = await headers();
const host = headersList.get("host") ?? undefined;
const posthog = new PostHog(env.POSTHOG_PAPIK, {
host: 'https://us.i.posthog.com',
@ -63,7 +96,12 @@ export async function captureEvent<E extends PosthogEvent>(event: E, properties:
posthog.capture({
event,
properties,
distinctId: cookie?.distinct_id ?? '',
properties: {
...properties,
sourcebot_version: clientEnv.NEXT_PUBLIC_SOURCEBOT_VERSION,
install_id: env.SOURCEBOT_INSTALL_ID,
$host: host,
},
distinctId,
});
}

View file

@ -29,71 +29,10 @@ export type PosthogEventMap = {
fileLanguages: string[],
isSearchExhaustive: boolean
},
share_link_created: {},
////////////////////////////////////////////////////////////////
wa_secret_created_success: {
key: string,
},
wa_secret_deleted_success: {
key: string,
},
wa_secret_deleted_fail: {
key: string,
error: string,
},
wa_secret_created_fail: {
key: string,
error: string,
},
wa_secret_fetch_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_warning_nav_connection_fetch_fail: {
error: string,
},
wa_warning_nav_hover: {},
wa_warning_nav_pressed: {},
wa_warning_nav_connection_pressed: {},
//////////////////////////////////////////////////////////////////
wa_error_nav_connection_fetch_fail: {
error: string,
},
wa_error_nav_hover: {},
wa_error_nav_pressed: {},
wa_error_nav_job_pressed: {},
wa_error_nav_job_fetch_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_progress_nav_connection_fetch_fail: {
error: string,
},
wa_progress_nav_repo_fetch_fail: {
error: string,
},
wa_progress_nav_hover: {},
wa_progress_nav_pressed: {},
wa_progress_nav_job_pressed: {},
//////////////////////////////////////////////////////////////////
wa_trial_nav_pressed: {},
wa_trial_nav_subscription_fetch_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_connection_list_item_error_hover: {},
wa_connection_list_item_error_pressed: {},
wa_connection_list_item_warning_hover: {},
wa_connection_list_item_warning_pressed: {},
//////////////////////////////////////////////////////////////////
wa_connection_list_item_manage_pressed: {},
//////////////////////////////////////////////////////////////////
wa_create_connection_success: {
type: string,
},
wa_create_connection_fail: {
type: string,
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_config_editor_quick_action_pressed: {
@ -101,124 +40,64 @@ export type PosthogEventMap = {
type: string,
},
//////////////////////////////////////////////////////////////////
wa_secret_combobox_import_secret_pressed: {
type: string,
},
wa_secret_combobox_import_secret_success: {
type: string,
},
wa_secret_combobox_import_secret_fail: {
type: string,
error: string,
},
//////////////////////////////////////////////////////////////////
wa_billing_email_updated_success: {},
wa_billing_email_updated_fail: {
error: string,
},
wa_billing_email_fetch_fail: {
error: string,
errorCode: string,
},
wa_manage_subscription_button_create_portal_session_success: {},
wa_manage_subscription_button_create_portal_session_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_invite_member_card_invite_success: {
num_emails: number,
},
wa_invite_member_card_invite_fail: {
error: string,
errorCode: string,
num_emails: number,
},
wa_invite_member_card_invite_cancel: {
num_emails: number,
},
//////////////////////////////////////////////////////////////////
wa_onboard_skip_onboarding: {
step: string,
},
wa_onboard_invite_team_invite_success: {
num_emails: number,
},
wa_onboard_invite_team_invite_fail: {
error: string,
num_emails: number,
},
wa_onboard_invite_team_skip: {
num_emails: number,
},
//////////////////////////////////////////////////////////////////
wa_members_list_remove_member_success: {},
wa_members_list_remove_member_fail: {
error: string,
errorCode: string,
},
wa_members_list_transfer_ownership_success: {},
wa_members_list_transfer_ownership_fail: {
error: string,
errorCode: string,
},
wa_members_list_leave_org_success: {},
wa_members_list_leave_org_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_invites_list_cancel_invite_success: {},
wa_invites_list_cancel_invite_fail: {
error: string,
errorCode: string,
},
wa_invites_list_copy_invite_link_success: {},
wa_invites_list_copy_invite_link_fail: {},
wa_invites_list_copy_email_success: {},
wa_invites_list_copy_email_fail: {},
//////////////////////////////////////////////////////////////////
wa_onboard_org_create_success: {},
wa_onboard_org_create_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_connect_code_host_button_pressed: {
name: string,
},
//////////////////////////////////////////////////////////////////
wa_onboard_checkout_success: {},
wa_onboard_checkout_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_team_upgrade_card_pressed: {},
wa_team_upgrade_checkout_success: {},
wa_team_upgrade_checkout_fail: {
error: string,
errorCode: string,
},
wa_enterprise_upgrade_card_pressed: {},
//////////////////////////////////////////////////////////////////
wa_connection_delete_success: {},
wa_connection_delete_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_connection_failed_status_hover: {},
wa_connection_retry_sync_success: {},
wa_connection_retry_sync_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_connection_not_found_warning_displayed: {},
wa_connection_secrets_navigation_pressed: {},
//////////////////////////////////////////////////////////////////
wa_connection_retry_all_failed_repos_pressed: {},
wa_connection_retry_all_failed_repos_fetch_fail: {
error: string,
},
wa_connection_retry_all_failed_repos_fail: {},
wa_connection_retry_all_failed_repos_success: {},
wa_connection_retry_all_failed_no_repos: {},
//////////////////////////////////////////////////////////////////
wa_repo_retry_index_success: {},
wa_repo_retry_index_fail: {
error: string,
},
//////////////////////////////////////////////////////////////////
wa_login_with_github: {},
wa_login_with_google: {},
wa_login_with_gitlab: {},
@ -235,24 +114,16 @@ export type PosthogEventMap = {
//////////////////////////////////////////////////////////////////
wa_org_name_updated_success: {},
wa_org_name_updated_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_org_domain_updated_success: {},
wa_org_domain_updated_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_onboard_github_selected: {},
wa_onboard_gitlab_selected: {},
wa_onboard_gitea_selected: {},
wa_onboard_gerrit_selected: {},
wa_onboard_bitbucket_cloud_selected: {},
wa_onboard_bitbucket_server_selected: {},
//////////////////////////////////////////////////////////////////
wa_security_page_click: {},
//////////////////////////////////////////////////////////////////
wa_demo_card_click: {},
wa_demo_try_card_pressed: {},
wa_share_link_created: {},
//////////////////////////////////////////////////////////////////
@ -262,12 +133,12 @@ export type PosthogEventMap = {
//////////////////////////////////////////////////////////////////
wa_requests_list_approve_request_success: {},
wa_requests_list_approve_request_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_requests_list_reject_request_success: {},
wa_requests_list_reject_request_fail: {
error: string,
errorCode: string,
},
//////////////////////////////////////////////////////////////////
wa_api_key_created: {},
@ -281,11 +152,6 @@ export type PosthogEventMap = {
wa_chat_thread_created: {},
//////////////////////////////////////////////////////////////////
wa_demo_docs_link_pressed: {},
wa_demo_search_context_card_pressed: {
contextType: string,
contextName: string,
contextDisplayName: string,
},
wa_demo_search_example_card_pressed: {
exampleTitle: string,
exampleUrl: string,

View file

@ -62,10 +62,18 @@ export const createPathWithQueryParams = (path: string, ...queryParams: [string,
return path;
}
const queryString = queryParams.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value ?? '')}`).join('&');
const queryString = queryParams.map(([key, value]) => `${encodeURIComponent(key)}=${encodeRFC3986URIComponent(value ?? '')}`).join('&');
return `${path}?${queryString}`;
}
// @see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
const encodeRFC3986URIComponent = (str: string) => {
return encodeURIComponent(str).replace(
/[!'()*]/g,
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
);
}
type AuthProviderInfo = {
id: string;
name: string;

View file

@ -156,7 +156,7 @@ export const getAuthenticatedUser = async () => {
/**
* Returns a API key object if the API key string is valid, otherwise returns undefined.
*/
const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => {
export const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => {
const parts = apiKeyString.split("-");
if (parts.length !== 2 || parts[0] !== "sourcebot") {
return undefined;

View file

@ -3294,10 +3294,10 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:15.5.7":
version: 15.5.7
resolution: "@next/env@npm:15.5.7"
checksum: 10c0/f92d99e5fa3516c6b7699abafd9bd813f5c1889dd257ab098f1b71f93137f5e4f49792e22f6dddf8a59efcb134e8e84277c983ff88607b2a42aac651bfde78ea
"@next/env@npm:15.5.9":
version: 15.5.9
resolution: "@next/env@npm:15.5.9"
checksum: 10c0/92c4e29d81a8e78c33c2da179648a4f478a9a6852966192e079007b19ec9955e72530d5ca7df55ea0efeccbf5b1c9d0efcaf80433e26af89c6478193e1d088f1
languageName: node
linkType: hard
@ -8243,7 +8243,7 @@ __metadata:
langfuse-vercel: "npm:^3.38.4"
lucide-react: "npm:^0.517.0"
micromatch: "npm:^4.0.8"
next: "npm:^15.5.7"
next: "npm:15.5.9"
next-auth: "npm:^5.0.0-beta.30"
next-navigation-guard: "npm:^0.2.0"
next-themes: "npm:^0.3.0"
@ -16178,11 +16178,11 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^15.5.7":
version: 15.5.7
resolution: "next@npm:15.5.7"
"next@npm:15.5.9":
version: 15.5.9
resolution: "next@npm:15.5.9"
dependencies:
"@next/env": "npm:15.5.7"
"@next/env": "npm:15.5.9"
"@next/swc-darwin-arm64": "npm:15.5.7"
"@next/swc-darwin-x64": "npm:15.5.7"
"@next/swc-linux-arm64-gnu": "npm:15.5.7"
@ -16233,7 +16233,7 @@ __metadata:
optional: true
bin:
next: dist/bin/next
checksum: 10c0/baf5b9f42416c478702b3894479b3d7862bc4abf18afe0e43b7fc7ed35567b8dc6cb76cd94906505bab9013cb8d0f3370cdc0451c01ec15ae5a638d37b5ba7c7
checksum: 10c0/6a120afbc45b96aa14debba6375602d6319093af4e3e8c648cf22b12ffb9db016c889df5e764cf5e0aa414ad60505db4e2095624a19f4b71316561076158651a
languageName: node
linkType: hard