Skip to content

Commit 69dc7bc

Browse files
authored
feat(webapp): Vercel / Slack integrations improvements (#3108)
## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing Slack + GitHub + Vercel + Builds + Deployments --- ## Changelog Settings changes: - Split general from integrations - Add new Slack section to org level integrations Vercel improvements: - bugfix for TRIGGER_SECRET_KEY collision - onboarding improvements for connecting to projects - new loops event Slack improvements: - nicer alerts Webhook/Email alerts: - rich events with Github & Vercel integration data --- ## Screenshots <img width="2550" height="652" alt="Screenshot 2026-02-20 at 21 53 34" src="https://github.com/user-attachments/assets/8d7c9f1d-5fe9-4516-8fb3-885460b4207f" /> <img width="843" height="710" alt="Screenshot 2026-02-23 at 10 55 54" src="https://github.com/user-attachments/assets/8ea72c1f-431b-493c-b9a9-8076cce12262" /> <img width="765" height="466" alt="Screenshot 2026-02-20 at 21 52 46" src="https://github.com/user-attachments/assets/157fafb8-b7bf-499d-8953-c2aed5e44ce0" /> <img width="691" height="261" alt="Screenshot 2026-02-20 at 22 04 24" src="https://github.com/user-attachments/assets/3aea7369-2008-4af8-a9c0-5fbfa2cc381d" /> <img width="2032" height="1114" alt="Screenshot 2026-02-19 at 14 48 49" src="https://github.com/user-attachments/assets/dc10c14e-cd15-445a-b5be-d694d29d20e5" /> <img width="2032" height="1114" alt="Screenshot 2026-02-19 at 14 49 04" src="https://github.com/user-attachments/assets/1ef591fd-fd00-430a-9649-8b18cff9586d" /> <img width="1583" height="1115" alt="Screenshot 2026-02-19 at 17 32 56" src="https://github.com/user-attachments/assets/c5c8f318-d193-4dd4-86f7-1cc4bbcc4e0c" /> <img width="422" height="187" alt="Screenshot 2026-02-20 at 21 57 41" src="https://github.com/user-attachments/assets/37865cb6-4c0d-40ef-9c60-7b057d546c61" /> <img width="1583" height="1115" alt="Screenshot 2026-02-19 at 17 33 06" src="https://github.com/user-attachments/assets/e9180e8e-e611-4734-9232-80c62ff863ad" /> 💯
1 parent 3a70546 commit 69dc7bc

File tree

29 files changed

+1891
-839
lines changed

29 files changed

+1891
-839
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { VercelLogo } from "./VercelLogo";
2+
import { LinkButton } from "~/components/primitives/Buttons";
3+
import { SimpleTooltip } from "~/components/primitives/Tooltip";
4+
5+
export function VercelLink({ vercelDeploymentUrl }: { vercelDeploymentUrl: string }) {
6+
return (
7+
<SimpleTooltip
8+
button={
9+
<LinkButton
10+
variant="minimal/small"
11+
LeadingIcon={<VercelLogo className="size-3.5" />}
12+
iconSpacing="gap-x-1"
13+
to={vercelDeploymentUrl}
14+
className="pl-1"
15+
>
16+
Vercel
17+
</LinkButton>
18+
}
19+
content="View on Vercel"
20+
/>
21+
);
22+
}

apps/webapp/app/components/integrations/VercelOnboardingModal.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
} from "~/v3/vercel/vercelProjectIntegrationSchema";
4545
import { type VercelCustomEnvironment } from "~/models/vercelIntegration.server";
4646
import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresenter.server";
47-
import { vercelAppInstallPath, v3ProjectSettingsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
47+
import { vercelAppInstallPath, v3ProjectSettingsIntegrationsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
4848
import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
4949
import { useEffect, useState, useCallback, useRef } from "react";
5050
import { usePostHogTracking } from "~/hooks/usePostHog";
@@ -102,6 +102,7 @@ export function VercelOnboardingModal({
102102
hasOrgIntegration,
103103
nextUrl,
104104
onDataReload,
105+
vercelManageAccessUrl,
105106
}: {
106107
isOpen: boolean;
107108
onClose: () => void;
@@ -114,6 +115,7 @@ export function VercelOnboardingModal({
114115
hasOrgIntegration: boolean;
115116
nextUrl?: string;
116117
onDataReload?: (vercelStagingEnvironment?: string) => void;
118+
vercelManageAccessUrl?: string;
117119
}) {
118120
const { capture, startSessionRecording } = usePostHogTracking();
119121
const navigation = useNavigation();
@@ -122,7 +124,8 @@ export function VercelOnboardingModal({
122124
const completeOnboardingFetcher = useFetcher();
123125
const { Form: CompleteOnboardingForm } = completeOnboardingFetcher;
124126
const [searchParams] = useSearchParams();
125-
const fromMarketplaceContext = searchParams.get("origin") === "marketplace";
127+
const origin = searchParams.get("origin");
128+
const fromMarketplaceContext = origin === "marketplace";
126129

127130
const availableProjects = onboardingData?.availableProjects || [];
128131
const hasProjectSelected = onboardingData?.hasProjectSelected ?? false;
@@ -543,8 +546,15 @@ export function VercelOnboardingModal({
543546

544547
if (!isGitHubConnectedForOnboarding) {
545548
setState("github-connection");
549+
capture("vercel onboarding github step viewed", {
550+
origin: fromMarketplaceContext ? "marketplace" : "dashboard",
551+
step: "github-connection",
552+
organization_slug: organizationSlug,
553+
project_slug: projectSlug,
554+
github_app_installed: gitHubAppInstallations.length > 0,
555+
});
546556
}
547-
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding]);
557+
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding, capture, organizationSlug, projectSlug, gitHubAppInstallations.length]);
548558

549559
const handleFinishOnboarding = useCallback((e: React.FormEvent<HTMLFormElement>) => {
550560
e.preventDefault();
@@ -639,7 +649,7 @@ export function VercelOnboardingModal({
639649
onClose();
640650
}
641651
}}>
642-
<DialogContent className="max-w-lg">
652+
<DialogContent className="max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
643653
<DialogHeader>
644654
<div className="flex items-center gap-2">
645655
<VercelLogo className="size-5" />
@@ -727,14 +737,25 @@ export function VercelOnboardingModal({
727737

728738
<FormButtons
729739
confirmButton={
730-
<Button
731-
variant="primary/medium"
732-
onClick={handleProjectSelection}
733-
disabled={!selectedVercelProject || fetcher.state !== "idle"}
734-
LeadingIcon={fetcher.state !== "idle" ? SpinnerWhite : undefined}
735-
>
736-
{fetcher.state !== "idle" ? "Connecting..." : "Connect Project"}
737-
</Button>
740+
<div className="flex items-center gap-2">
741+
{vercelManageAccessUrl && !origin && (
742+
<LinkButton
743+
to={vercelManageAccessUrl}
744+
variant="tertiary/medium"
745+
target="_self"
746+
>
747+
Manage access
748+
</LinkButton>
749+
)}
750+
<Button
751+
variant="primary/medium"
752+
onClick={handleProjectSelection}
753+
disabled={!selectedVercelProject || fetcher.state !== "idle"}
754+
LeadingIcon={fetcher.state !== "idle" ? SpinnerWhite : undefined}
755+
>
756+
{fetcher.state !== "idle" ? "Connecting..." : "Connect Project"}
757+
</Button>
758+
</div>
738759
}
739760
cancelButton={
740761
<Button
@@ -813,6 +834,7 @@ export function VercelOnboardingModal({
813834
<Header3>Pull Environment Variables</Header3>
814835
<Paragraph className="text-sm">
815836
Select which environment variables to pull from Vercel now. This is a one-time pull.
837+
Later on environment variables can be pulled before each build.
816838
</Paragraph>
817839

818840
<div className="flex gap-4 text-sm">
@@ -1057,7 +1079,7 @@ export function VercelOnboardingModal({
10571079
</Callout>
10581080

10591081
{(() => {
1060-
const baseSettingsPath = v3ProjectSettingsPath(
1082+
const baseSettingsPath = v3ProjectSettingsIntegrationsPath(
10611083
{ slug: organizationSlug },
10621084
{ slug: projectSlug },
10631085
{ slug: environmentSlug }
@@ -1081,6 +1103,7 @@ export function VercelOnboardingModal({
10811103
)}
10821104
variant="secondary/medium"
10831105
LeadingIcon={OctoKitty}
1106+
onClick={() => trackOnboarding("vercel onboarding github app install clicked")}
10841107
>
10851108
Install GitHub app
10861109
</LinkButton>
@@ -1110,6 +1133,7 @@ export function VercelOnboardingModal({
11101133
<Button
11111134
variant="primary/medium"
11121135
onClick={() => {
1136+
trackOnboarding("vercel onboarding github completed");
11131137
setState("completed");
11141138
const validUrl = safeRedirectUrl(nextUrl);
11151139
if (validUrl) {
@@ -1123,6 +1147,7 @@ export function VercelOnboardingModal({
11231147
<Button
11241148
variant="tertiary/medium"
11251149
onClick={() => {
1150+
trackOnboarding("vercel onboarding github skipped");
11261151
setState("completed");
11271152
if (fromMarketplaceContext && nextUrl) {
11281153
const validUrl = safeRedirectUrl(nextUrl);
@@ -1141,6 +1166,7 @@ export function VercelOnboardingModal({
11411166
<Button
11421167
variant="tertiary/medium"
11431168
onClick={() => {
1169+
trackOnboarding("vercel onboarding github skipped");
11441170
setState("completed");
11451171
}}
11461172
>

apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ import {
33
ChartBarIcon,
44
Cog8ToothIcon,
55
CreditCardIcon,
6-
PuzzlePieceIcon,
76
UserGroupIcon,
87
} from "@heroicons/react/20/solid";
98
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
9+
import { SlackIcon } from "@trigger.dev/companyicons";
10+
import { VercelLogo } from "~/components/integrations/VercelLogo";
1011
import { useFeatures } from "~/hooks/useFeatures";
1112
import { type MatchedOrganization } from "~/hooks/useOrganizations";
1213
import { cn } from "~/utils/cn";
1314
import {
1415
organizationSettingsPath,
16+
organizationSlackIntegrationPath,
1517
organizationTeamPath,
1618
organizationVercelIntegrationPath,
1719
rootPath,
@@ -115,13 +117,25 @@ export function OrganizationSettingsSideMenu({
115117
to={organizationSettingsPath(organization)}
116118
data-action="settings"
117119
/>
120+
</div>
121+
<div className="flex flex-col">
122+
<div className="mb-1">
123+
<SideMenuHeader title="Integrations" />
124+
</div>
118125
<SideMenuItem
119-
name="Integrations"
120-
icon={PuzzlePieceIcon}
121-
activeIconColor="text-blue-500"
126+
name="Vercel"
127+
icon={VercelLogo}
128+
activeIconColor="text-white"
122129
to={organizationVercelIntegrationPath(organization)}
123130
data-action="integrations"
124131
/>
132+
<SideMenuItem
133+
name="Slack"
134+
icon={SlackIcon}
135+
activeIconColor="text-white"
136+
to={organizationSlackIntegrationPath(organization)}
137+
data-action="integrations"
138+
/>
125139
</div>
126140
<div className="flex flex-col gap-1">
127141
<SideMenuHeader title="App version" />

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Cog8ToothIcon,
1212
CogIcon,
1313
ExclamationTriangleIcon,
14+
PuzzlePieceIcon,
1415
FolderIcon,
1516
FolderOpenIcon,
1617
GlobeAmericasIcon,
@@ -74,7 +75,8 @@ import {
7475
v3LogsPath,
7576
v3ProjectAlertsPath,
7677
v3ProjectPath,
77-
v3ProjectSettingsPath,
78+
v3ProjectSettingsGeneralPath,
79+
v3ProjectSettingsIntegrationsPath,
7880
v3QueuesPath,
7981
v3RunsPath,
8082
v3SchedulesPath,
@@ -589,13 +591,34 @@ export function SideMenu({
589591
data-action="limits"
590592
isCollapsed={isCollapsed}
591593
/>
594+
</SideMenuSection>
595+
596+
<SideMenuSection
597+
title="Project settings"
598+
isSideMenuCollapsed={isCollapsed}
599+
itemSpacingClassName="space-y-0"
600+
initialCollapsed={getSectionCollapsed(
601+
user.dashboardPreferences.sideMenu,
602+
"project-settings"
603+
)}
604+
onCollapseToggle={handleSectionToggle("project-settings")}
605+
>
592606
<SideMenuItem
593-
name="Project settings"
607+
name="General"
594608
icon={Cog8ToothIcon}
595609
activeIconColor="text-text-bright"
596610
inactiveIconColor="text-text-dimmed"
597-
to={v3ProjectSettingsPath(organization, project, environment)}
598-
data-action="project-settings"
611+
to={v3ProjectSettingsGeneralPath(organization, project, environment)}
612+
data-action="project-settings-general"
613+
isCollapsed={isCollapsed}
614+
/>
615+
<SideMenuItem
616+
name="Integrations"
617+
icon={PuzzlePieceIcon}
618+
activeIconColor="text-text-bright"
619+
inactiveIconColor="text-text-dimmed"
620+
to={v3ProjectSettingsIntegrationsPath(organization, project, environment)}
621+
data-action="project-settings-integrations"
599622
isCollapsed={isCollapsed}
600623
/>
601624
</SideMenuSection>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22

33
// Valid section IDs that can have their collapsed state toggled
4-
export const SideMenuSectionIdSchema = z.enum(["manage", "metrics"]);
4+
export const SideMenuSectionIdSchema = z.enum(["manage", "metrics", "project-settings"]);
55

66
// Inferred type from the schema
77
export type SideMenuSectionId = z.infer<typeof SideMenuSectionIdSchema>;

apps/webapp/app/models/vercelIntegration.server.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,13 @@ export class VercelIntegrationRepository {
975975
return { created: 0, updated: 0, errors: [] as string[] };
976976
}
977977

978+
await this.removeAllVercelEnvVarsByKey({
979+
client,
980+
vercelProjectId: params.vercelProjectId,
981+
teamId: params.teamId,
982+
key: "TRIGGER_SECRET_KEY",
983+
});
984+
978985
const result = await this.batchUpsertVercelEnvVars({
979986
client,
980987
vercelProjectId: params.vercelProjectId,
@@ -1526,6 +1533,35 @@ export class VercelIntegrationRepository {
15261533
return { created, updated, errors };
15271534
}
15281535

1536+
private static async removeAllVercelEnvVarsByKey(params: {
1537+
client: Vercel;
1538+
vercelProjectId: string;
1539+
teamId: string | null;
1540+
key: string;
1541+
}): Promise<void> {
1542+
const { client, vercelProjectId, teamId, key } = params;
1543+
1544+
const existingEnvs = await client.projects.filterProjectEnvs({
1545+
idOrName: vercelProjectId,
1546+
...(teamId && { teamId }),
1547+
});
1548+
1549+
const envs = extractVercelEnvs(existingEnvs);
1550+
const idsToRemove = envs
1551+
.filter((env) => env.key === key && env.id)
1552+
.map((env) => env.id!);
1553+
1554+
if (idsToRemove.length === 0) {
1555+
return;
1556+
}
1557+
1558+
await client.projects.batchRemoveProjectEnv({
1559+
idOrName: vercelProjectId,
1560+
...(teamId && { teamId }),
1561+
requestBody: { ids: idsToRemove },
1562+
});
1563+
}
1564+
15291565
private static async upsertVercelEnvVar(params: {
15301566
client: Vercel;
15311567
vercelProjectId: string;

apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
1010
import { type User } from "~/models/user.server";
1111
import { processGitMetadata } from "./BranchesPresenter.server";
1212
import { BranchTrackingConfigSchema, getTrackedBranchForEnvironment } from "~/v3/github";
13-
import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema";
13+
import {
14+
VercelProjectIntegrationDataSchema,
15+
buildVercelDeploymentUrl,
16+
} from "~/v3/vercel/vercelProjectIntegrationSchema";
1417

1518
const pageSize = 20;
1619

@@ -232,8 +235,11 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
232235

233236
let vercelDeploymentUrl: string | null = null;
234237
if (hasVercelIntegration && deployment.integrationDeploymentId && vercelTeamSlug && vercelProjectName) {
235-
const vercelId = deployment.integrationDeploymentId.replace(/^dpl_/, "");
236-
vercelDeploymentUrl = `https://vercel.com/${vercelTeamSlug}/${vercelProjectName}/${vercelId}`;
238+
vercelDeploymentUrl = buildVercelDeploymentUrl(
239+
vercelTeamSlug,
240+
vercelProjectName,
241+
deployment.integrationDeploymentId
242+
);
237243
}
238244

239245
return {

0 commit comments

Comments
 (0)