-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Improve HubSpot integration #2874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds per-installation HubSpot settings to InstalledIntegration, exposes them through types and pages, provides a UI and server action to update closedWonDealStageId, and wires settings into webhook and tracking flows so sale tracking validates against a configurable deal stage. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as HubSpotSettings (Client)
participant SA as updateHubSpotSettingsAction (Server)
participant DB as InstalledIntegration.settings
participant Page as Settings Page
User->>UI: Enter closedWonDealStageId + Save
UI->>SA: submit { workspaceId, closedWonDealStageId|null }
SA->>DB: Update InstalledIntegration.settings
DB-->>SA: OK
SA->>Page: Revalidate / refresh
SA-->>UI: Success
UI-->>User: Toast: saved
sequenceDiagram
autonumber
participant HS as HubSpot
participant API as /api/hubspot/webhook
participant Val as hubSpotSettingsSchema
participant Sale as trackHubSpotSaleEvent
HS->>API: webhook (object.propertyChange, deal stage)
API->>Val: Parse installation.settings
Val-->>API: { closedWonDealStageId? }
API->>Sale: track(..., closedWonDealStageId)
Sale-->>API: Validate stage matches expected
API-->>HS: 200 or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)apps/web/lib/integrations/hubspot/track-lead.ts (6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (10)
apps/web/lib/integrations/hubspot/track-lead.ts (1)
84-90: Avoid "undefined undefined" names and use dub_id as clickId when available.Template-literal concatenation can emit
"undefined undefined". Also, we’re losing attribution by hardcodingclickId: ""when the contact may havedub_id.Apply:
- return await trackLead({ - clickId: "", - eventName: `Deal ${properties.dealstage}`, - customerExternalId: contactInfo.properties.email, - customerName: `${contactInfo.properties.firstname} ${contactInfo.properties.lastname}`, - customerEmail: contactInfo.properties.email, - mode: "async", - workspace, - rawBody: payload, - }); + const c = contactInfo.properties; + const customerName = + [c.firstname, c.lastname].filter(Boolean).join(" ") || null; + return await trackLead({ + clickId: c.dub_id ?? "", + eventName: `Deal ${properties.dealstage}`, + customerExternalId: c.email, + customerName, + customerEmail: c.email, + mode: "async", + workspace, + rawBody: payload, + });apps/web/lib/integrations/hubspot/schema.ts (1)
14-20: Trim and validate the optional deal-stage ID to reduce user input errors.Consider:
-export const hubSpotSettingsSchema = z.object({ - closedWonDealStageId: z - .string() - .nullish() - .describe("The ID of the deal stage that represents a closed won deal."), -}); +export const hubSpotSettingsSchema = z.object({ + closedWonDealStageId: z + .string() + .trim() + .min(1) + .nullish() + .describe("The ID of the deal stage that represents a closed won deal."), +});apps/web/lib/types.ts (1)
382-382: Exposingsettings?: Prisma.JsonValue— LGTM; consider narrowing at use sites.Where you consume settings (e.g., HubSpot UI/action), prefer a narrow type (e.g.,
{ closedWonDealStageId?: string | null }) to avoidanycasts and catch typos at compile time.apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsx (2)
277-281: Brand capitalization nit: “HubSpot”.- title="Hubspot integration is only available on Advanced plans and above. Upgrade to get started." + title="HubSpot integration is only available on Advanced plans and above. Upgrade to get started."
50-50: Use barrel export from @dub/utils
HUBSPOT_INTEGRATION_ID is re-exported by packages/utils/src/index.ts (export * from './constants'), so replace the deep import:Replace
import { HUBSPOT_INTEGRATION_ID } from "@dub/utils/src/constants/integrations";with
import { HUBSPOT_INTEGRATION_ID } from "@dub/utils";apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
20-50: Consider adding support forX-HubSpot-Signature-v3.HubSpot’s newer v3 signature uses HMAC with additional components; accepting both headers improves robustness during/after HS migrations.
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (1)
10-15: Remove unusedworkspaceIdinput to avoid trusting client-provided tenant IDs.You already have
ctx.workspace; dropworkspaceIdfrom the schema and thezimport.-import { z } from "zod"; -import { hubSpotSettingsSchema } from "./schema"; +import { hubSpotSettingsSchema } from "./schema"; -const schema = hubSpotSettingsSchema - .pick({ closedWonDealStageId: true }) - .extend({ - workspaceId: z.string(), - }); +const schema = hubSpotSettingsSchema.pick({ closedWonDealStageId: true });apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page.tsx (1)
60-62: Extract settings logic for consistency.The pattern mirrors the existing
credentialsextraction. The logic is correct but could benefit from consolidation.Consider extracting the installation data once to reduce duplication:
const installed = integration.installations.length > 0; + const installation = installed ? integration.installations[0] : undefined; const credentials = installed - ? integration.installations[0]?.credentials + ? installation?.credentials : undefined; const settings = installed - ? integration.installations[0]?.settings + ? installation?.settings : undefined; // TODO: // Fix this, we only displaying the first webhook only const webhookId = installed - ? integration.installations[0]?.webhooks[0]?.id + ? installation?.webhooks[0]?.id : undefined;apps/web/lib/integrations/hubspot/ui/settings.tsx (2)
17-19: Consider safer type casting and input validation.The type casting
(settings as any)?.closedWonDealStageIdbypasses TypeScript's type safety. While functional, this could be improved with proper typing.Consider one of these approaches for better type safety:
Option 1: Define a settings type interface:
interface HubSpotSettingsType { closedWonDealStageId?: string; } const [closedWonDealStageId, setClosedWonDealStageId] = useState( (settings as HubSpotSettingsType)?.closedWonDealStageId || DEFAULT_CLOSED_WON_DEAL_STAGE_ID, );Option 2: Use safe property access:
const [closedWonDealStageId, setClosedWonDealStageId] = useState(() => { if (settings && typeof settings === 'object' && 'closedWonDealStageId' in settings) { return (settings as { closedWonDealStageId?: string }).closedWonDealStageId || DEFAULT_CLOSED_WON_DEAL_STAGE_ID; } return DEFAULT_CLOSED_WON_DEAL_STAGE_ID; });
64-72: Add input validation for HubSpot deal stage format.Based on the HubSpot API documentation, deal stage IDs can be descriptive strings like "closedwon" for default pipelines or numeric strings for custom pipelines. Consider adding client-side validation to guide users.
Add basic input validation and helper text:
<div className="relative mt-4 rounded-md shadow-sm"> <input className="w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm" - placeholder={`Enter deal stage ID (e.g., ${DEFAULT_CLOSED_WON_DEAL_STAGE_ID})`} + placeholder={`Enter deal stage ID (e.g., ${DEFAULT_CLOSED_WON_DEAL_STAGE_ID} or 139921)`} type="text" autoComplete="off" name="closedWonDealStageId" value={closedWonDealStageId} onChange={(e) => setClosedWonDealStageId(e.target.value)} /> </div> + <p className="mt-2 text-xs text-neutral-500"> + Use descriptive names like "closedwon" for default pipelines, or numeric IDs for custom pipelines. + Find your deal stage ID in HubSpot's pipeline settings. + </p>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/web/app/(ee)/api/hubspot/webhook/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page.tsx(2 hunks)apps/web/lib/integrations/hubspot/constants.ts(1 hunks)apps/web/lib/integrations/hubspot/schema.ts(2 hunks)apps/web/lib/integrations/hubspot/track-lead.ts(2 hunks)apps/web/lib/integrations/hubspot/track-sale.ts(3 hunks)apps/web/lib/integrations/hubspot/ui/settings.tsx(1 hunks)apps/web/lib/integrations/hubspot/update-hubspot-settings.ts(1 hunks)apps/web/lib/types.ts(1 hunks)packages/prisma/schema/integration.prisma(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Only use components exported by hubspot/ui-extensions in settings components
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Do not use React components from hubspot/ui-extensions/crm in settings components
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Do not use window.fetch in settings components; use hubspot.fetch from hubspot/ui-extensions
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Settings components must not access the global window object
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
🧬 Code graph analysis (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsx (2)
packages/utils/src/constants/integrations.ts (1)
HUBSPOT_INTEGRATION_ID(6-6)apps/web/lib/integrations/hubspot/ui/settings.tsx (1)
HubSpotSettings(12-90)
apps/web/app/(ee)/api/hubspot/webhook/route.ts (2)
apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotSettingsSchema(15-20)apps/web/lib/integrations/hubspot/track-sale.ts (1)
trackHubSpotSaleEvent(9-87)
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (2)
apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotSettingsSchema(15-20)packages/utils/src/constants/integrations.ts (1)
HUBSPOT_INTEGRATION_ID(6-6)
apps/web/lib/integrations/hubspot/ui/settings.tsx (3)
apps/web/lib/types.ts (1)
InstalledIntegrationInfoProps(355-384)apps/web/lib/integrations/hubspot/constants.ts (1)
DEFAULT_CLOSED_WON_DEAL_STAGE_ID(27-27)apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (1)
updateHubSpotSettingsAction(16-46)
apps/web/lib/integrations/hubspot/track-sale.ts (3)
apps/web/lib/types.ts (1)
WorkspaceProps(186-202)apps/web/lib/integrations/hubspot/types.ts (1)
HubSpotAuthToken(4-4)apps/web/lib/integrations/hubspot/constants.ts (1)
DEFAULT_CLOSED_WON_DEAL_STAGE_ID(27-27)
apps/web/lib/integrations/hubspot/track-lead.ts (1)
apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotLeadEventSchema(67-71)
🔇 Additional comments (12)
apps/web/lib/integrations/hubspot/schema.ts (1)
37-38:dub_idrelaxed to nullish — LGTM.Permits undefined or null, matching upstream variability.
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsx (1)
56-61: Settings component wiring — LGTM.HubSpot appears correctly added to the integration‐component map.
apps/web/lib/integrations/hubspot/constants.ts (1)
27-27: Default Closed Won stage ID — LGTM.apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
128-135: Wiring settings into sale tracking — LGTM.Parsing installation settings and passing
closedWonDealStageIdmakes the stage check configurable with a sane default downstream.packages/prisma/schema/integration.prisma (1)
38-38: Confirm Prisma migration adding InstalledIntegration.settings exists and is applied.Adding
settings Json?(packages/prisma/schema/integration.prisma ≈ line 38) is fine; automated search returned no migration matches ("No files were searched"), so verify a committed + deployed Prisma migration that adds this column (or provide the migration file path). Ensure reads tolerate null.apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page.tsx (1)
89-89: LGTM!Settings are properly extracted and passed to the client component for display and configuration.
apps/web/lib/integrations/hubspot/track-sale.ts (4)
3-3: LGTM!The import is correctly added to support the configurable deal stage functionality.
13-13: LGTM!The function signature is properly updated to support configurable deal stage IDs with appropriate type annotations and optional parameter handling.
Also applies to: 18-18
20-21: LGTM!The fallback logic correctly uses the default constant when no custom deal stage ID is provided, ensuring backward compatibility.
36-41: Enhanced validation improves debugging.The dynamic comparison against the configurable deal stage ID and the enhanced error message with the expected value will significantly improve debugging when webhook validation fails.
Based on the web search results, the dealstage property returns internal IDs, and for the default "Sales Pipeline," "Closed Won" has an internal ID of "closedwon". However, for custom pipelines, these internal IDs are typically numeric strings rather than descriptive names. This change properly accommodates both scenarios.
apps/web/lib/integrations/hubspot/ui/settings.tsx (2)
30-41: LGTM!Form submission logic correctly validates workspace presence and handles empty string conversion to null appropriately. The implementation properly calls the server action with the expected parameters.
43-45: LGTM!Proper guard clause ensures the component only renders when the integration is installed, preventing unnecessary UI display.
…il` properties to contact updates and modifying the contact fetching logic to include these new fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/lib/integrations/hubspot/track-lead.ts (1)
40-53: Guard against missing email to avoid Prisma errors; fall back to contact.idWithout this,
customerExternalIdcan beundefined/null, causing DB lookup failures.const customerName = [properties.firstname, properties.lastname].filter(Boolean).join(" ") || null; - const trackLeadResult = await trackLead({ + const customerEmail = properties.email || null; + const customerExternalId = + customerEmail ?? String(contact.id); + + const trackLeadResult = await trackLead({ clickId: properties.dub_id, eventName: "Sign up", - customerEmail: properties.email, - customerExternalId: properties.email, + customerEmail, + customerExternalId, customerName, mode: "deferred", workspace, rawBody: payload, });
🧹 Nitpick comments (5)
apps/web/lib/integrations/hubspot/get-contact.ts (2)
12-19: Harden the request URL: encode contactId, use URLSearchParams, and set Accept headerPrevents path issues with non-numeric IDs and improves readability. Also explicitly request JSON.
Apply this diff:
- const response = await fetch( - `${HUBSPOT_API_HOST}/crm/v3/objects/contacts/${contactId}?properties=email,firstname,lastname,dub_id,dub_link,dub_partner_email`, - { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); + const params = new URLSearchParams({ + properties: [ + "email", + "firstname", + "lastname", + "dub_id", + "dub_link", + "dub_partner_email", + ].join(","), + }); + const response = await fetch( + `${HUBSPOT_API_HOST}/crm/v3/objects/contacts/${encodeURIComponent(String(contactId))}?${params.toString()}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + }, + );
28-30: More robust error surface on non-OK responsesFall back to the raw response payload if
messageis absent.- if (!response.ok) { - throw new Error(result.message); - } + if (!response.ok) { + throw new Error(result.message || JSON.stringify(result)); + }apps/web/lib/integrations/hubspot/track-lead.ts (1)
137-148: Don’t throw in background task if partner missingUse
findUniqueand guard email to avoid unhandled rejections inwaitUntil.- if (trackLeadResult.link?.partnerId) { - const partner = await prisma.partner.findUniqueOrThrow({ + if (trackLeadResult.link?.partnerId) { + const partner = await prisma.partner.findUnique({ where: { id: trackLeadResult.link.partnerId, }, select: { email: true, }, }); - - partnerEmail = partner.email ?? ""; + if (partner?.email) { + partnerEmail = partner.email; + } }apps/web/lib/integrations/hubspot/update-contact.ts (2)
15-17: Filter out empty-string values before PATCH to avoid wiping fieldsOnly send keys with non-empty values.
- if (Object.keys(properties).length === 0) { - return null; - } + const filteredProperties = Object.fromEntries( + Object.entries(properties).filter( + ([, v]) => typeof v === "string" && v.length > 0, + ), + ) as { dub_link?: string; dub_partner_email?: string }; + if (Object.keys(filteredProperties).length === 0) { + return null; + } ... - body: JSON.stringify({ - properties, - }), + body: JSON.stringify({ + properties: filteredProperties, + }),Also applies to: 35-37
27-34: Harden request: encode contactId path segment and set Accept headerPrevents path issues and explicitly negotiates JSON.
- const response = await fetch( - `${HUBSPOT_API_HOST}/crm/v3/objects/contacts/${contactId}`, + const response = await fetch( + `${HUBSPOT_API_HOST}/crm/v3/objects/contacts/${encodeURIComponent(String(contactId))}`, { method: "PATCH", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", + Accept: "application/json", },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/lib/integrations/hubspot/get-contact.ts(1 hunks)apps/web/lib/integrations/hubspot/schema.ts(2 hunks)apps/web/lib/integrations/hubspot/track-lead.ts(5 hunks)apps/web/lib/integrations/hubspot/track-sale.ts(2 hunks)apps/web/lib/integrations/hubspot/update-contact.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/integrations/hubspot/schema.ts
- apps/web/lib/integrations/hubspot/track-sale.ts
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/lib/integrations/hubspot/get-contact.ts (1)
apps/web/lib/integrations/hubspot/constants.ts (1)
HUBSPOT_API_HOST(11-11)
apps/web/lib/integrations/hubspot/track-lead.ts (4)
apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotLeadEventSchema(69-73)apps/web/lib/api/conversions/track-lead.ts (1)
trackLead(29-362)apps/web/lib/types.ts (1)
TrackLeadResponse(401-401)apps/web/lib/integrations/hubspot/update-contact.ts (1)
updateHubSpotContact(3-56)
apps/web/lib/integrations/hubspot/update-contact.ts (1)
apps/web/lib/integrations/hubspot/constants.ts (1)
HUBSPOT_API_HOST(11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (5)
apps/web/lib/integrations/hubspot/get-contact.ts (1)
28-30: Harden error message fallback to avoid “undefined” errors.HubSpot errors aren’t always in
result.message. Prefer a richer fallback.- if (!response.ok) { - throw new Error(result.message); - } + if (!response.ok) { + const msg = + result?.message ?? + result?.reason ?? + result?.errors?.[0]?.message ?? + response.statusText ?? + "Failed to retrieve contact"; + throw new Error(`HubSpot ${response.status}: ${msg}`); + }apps/web/lib/integrations/hubspot/update-contact.ts (1)
43-45: Improve error detail on non‑2xx responses.HubSpot error payloads vary; include robust fallbacks for clearer diagnostics.
- if (!response.ok) { - throw new Error(result.message || "Failed to update contact"); - } + if (!response.ok) { + const msg = + result?.message ?? + result?.reason ?? + result?.errors?.[0]?.message ?? + response.statusText ?? + "Failed to update contact"; + throw new Error(`HubSpot ${response.status}: ${msg}`); + }apps/web/lib/integrations/hubspot/track-lead.ts (3)
55-63: Ensure background task doesn’t raise unhandled rejection.Attach a catch to the promise passed to
waitUntilto avoid noisy unhandled rejections.- if (trackLeadResult) { - waitUntil( - _updateHubSpotContact({ - contact: contactInfo, - trackLeadResult, - accessToken: authToken.access_token, - }), - ); - } + if (trackLeadResult) { + waitUntil( + _updateHubSpotContact({ + contact: contactInfo, + trackLeadResult, + accessToken: authToken.access_token, + }).catch((e) => { + console.error( + `[HubSpot] _updateHubSpotContact failed for contact ${contactInfo.id}:`, + e, + ); + }), + ); + }
99-105: Avoid “null null” customer names in 0‑3 flow.Mirror the safer join used in the 0‑1 branch.
- customerName: `${contactInfo.properties.firstname} ${contactInfo.properties.lastname}`, + customerName: + [contactInfo.properties.firstname, contactInfo.properties.lastname] + .filter(Boolean) + .join(" ") || null,
110-118: Also guard the second background task with a catch.- if (trackLeadResult) { - waitUntil( - _updateHubSpotContact({ - contact: contactInfo, - trackLeadResult, - accessToken: authToken.access_token, - }), - ); - } + if (trackLeadResult) { + waitUntil( + _updateHubSpotContact({ + contact: contactInfo, + trackLeadResult, + accessToken: authToken.access_token, + }).catch((e) => { + console.error( + `[HubSpot] _updateHubSpotContact failed for contact ${contactInfo.id}:`, + e, + ); + }), + ); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/lib/integrations/hubspot/get-contact.ts(1 hunks)apps/web/lib/integrations/hubspot/schema.ts(2 hunks)apps/web/lib/integrations/hubspot/track-lead.ts(5 hunks)apps/web/lib/integrations/hubspot/track-sale.ts(2 hunks)apps/web/lib/integrations/hubspot/types.ts(1 hunks)apps/web/lib/integrations/hubspot/update-contact.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/integrations/hubspot/schema.ts
- apps/web/lib/integrations/hubspot/track-sale.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-17T02:53:28.359Z
Learnt from: devkiran
PR: dubinc/dub#2839
File: apps/web/lib/integrations/hubspot/schema.ts:5-12
Timestamp: 2025-09-17T02:53:28.359Z
Learning: HubSpot's OAuth token response returns `scopes` as an array of strings, not as a space-delimited string. The schema `scopes: z.array(z.string())` in hubSpotAuthTokenSchema is correct for HubSpot's actual API response format.
Applied to files:
apps/web/lib/integrations/hubspot/types.ts
🧬 Code graph analysis (4)
apps/web/lib/integrations/hubspot/update-contact.ts (1)
apps/web/lib/integrations/hubspot/constants.ts (1)
HUBSPOT_API_HOST(11-11)
apps/web/lib/integrations/hubspot/get-contact.ts (1)
apps/web/lib/integrations/hubspot/constants.ts (1)
HUBSPOT_API_HOST(11-11)
apps/web/lib/integrations/hubspot/types.ts (1)
apps/web/lib/integrations/hubspot/schema.ts (3)
hubSpotAuthTokenSchema(5-12)hubSpotRefreshTokenSchema(22-28)hubSpotContactSchema(31-41)
apps/web/lib/integrations/hubspot/track-lead.ts (5)
apps/web/lib/integrations/hubspot/get-contact.ts (1)
getHubSpotContact(4-39)apps/web/lib/api/conversions/track-lead.ts (1)
trackLead(29-362)apps/web/lib/integrations/hubspot/types.ts (1)
HubSpotContact(12-12)apps/web/lib/types.ts (1)
TrackLeadResponse(401-401)apps/web/lib/integrations/hubspot/update-contact.ts (1)
updateHubSpotContact(3-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
apps/web/lib/integrations/hubspot/types.ts (1)
2-6: LGTM – exporting HubSpotContact type improves ergonomics.Import grouping and the new
HubSpotContactexport look good.Also applies to: 12-12
apps/web/lib/integrations/hubspot/get-contact.ts (1)
13-13: Confirmed — HubSpot GET tolerates unknown properties (returns 200 and omits them). HubSpot CRM v3 returns 200 and excludes any undefined properties from the response, so the current request is safe.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (2)
18-21: Validate input workspaceId against ctx.workspace.id.You accept workspaceId in the schema but don’t verify it matches the authenticated workspace. Add a guard to prevent accidental/misleading cross‑tenant updates.
- const { closedWonDealStageId } = parsedInput; + const { workspaceId, closedWonDealStageId } = parsedInput; + if (workspaceId !== workspace.id) { + throw new Error("Invalid workspaceId for this session."); + }
35-35: Avoidanyfor settings; use a safer shape.Minor type hygiene to reduce foot‑guns when extending settings.
- const current = (installedIntegration.settings as any) ?? {}; + const current = + ((installedIntegration.settings ?? {}) as Record<string, unknown>);apps/web/lib/integrations/hubspot/track-lead.ts (1)
44-66: Ensure robust background processing with error handling.The deferred contact update using
waitUntilis appropriate for non-blocking operations. However, consider adding error handling to prevent silent failures in the background task.Apply this diff to add error handling:
if (trackLeadResult) { waitUntil( - _updateHubSpotContact({ - contact: contactInfo, - trackLeadResult, - accessToken: authToken.access_token, - }), + _updateHubSpotContact({ + contact: contactInfo, + trackLeadResult, + accessToken: authToken.access_token, + }).catch((error) => { + console.error( + `[HubSpot] Failed to update contact ${contactInfo.id} in background:`, + error, + ); + }), ); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/lib/integrations/hubspot/track-lead.ts(5 hunks)apps/web/lib/integrations/hubspot/update-hubspot-settings.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/lib/integrations/hubspot/track-lead.ts (5)
apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotLeadEventSchema(69-73)apps/web/lib/integrations/hubspot/get-contact.ts (1)
getHubSpotContact(4-39)apps/web/lib/integrations/hubspot/types.ts (1)
HubSpotContact(12-12)apps/web/lib/types.ts (1)
TrackLeadResponse(401-401)apps/web/lib/integrations/hubspot/update-contact.ts (1)
updateHubSpotContact(3-52)
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (2)
apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotSettingsSchema(15-20)packages/utils/src/constants/integrations.ts (1)
HUBSPOT_INTEGRATION_ID(6-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (8)
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (3)
22-27: Tenant scoping fix looks good.Filtering by both integrationId and projectId correctly scopes to the current workspace.
37-47: Summary claims a default is applied, but code doesn’t.The AI summary mentions DEFAULT_CLOSED_WON_DEAL_STAGE_ID, but this action doesn’t apply any default when value is null/undefined. Confirm intended behavior; if a default is desired, import and apply it on nullish input.
49-50: Revalidation path is correct.This will refresh the HubSpot settings UI for the current workspace after update.
apps/web/lib/integrations/hubspot/track-lead.ts (5)
1-9: LGTM!The imports are correctly added for the new functionality. The
waitUntilutility allows asynchronous tasks to run after the response is sent without blocking the user's response, and the new imports support the deferred contact update functionality.
20-20: LGTM!The variable name change from
contacttocontactInfoprovides better clarity about the data being retrieved from HubSpot.
24-42: LGTM!The changes improve code clarity and robustness by:
- Using the more descriptive variable name
contactInfo- Adding proper null check for contact info
- Using the improved customer name derivation logic that filters out falsy values
99-121: LGTM! Customer name construction is improved.The customer name construction now properly handles the case where firstname or lastname might be missing, addressing the previous issue of "undefined undefined".
125-171: LGTM! Conditional property updates prevent field clearing.The implementation correctly addresses the previous issues by:
- Building properties conditionally to avoid sending empty strings
- Checking
partner.emailbefore adding to properties- Only calling
updateHubSpotContactwhen there are actual properties to updateThis prevents accidentally clearing existing HubSpot fields with empty values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (13)
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (1)
10-15: WorkspaceId input appears unused; remove to reduce confusion
ctx.workspace.idis used for scoping;workspaceIdin the schema isn’t referenced.Apply this diff:
-const schema = hubSpotSettingsSchema - .pick({ closedWonDealStageId: true }) - .extend({ - workspaceId: z.string(), - }); +const schema = hubSpotSettingsSchema.pick({ closedWonDealStageId: true });apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
128-135: Parse settings defensively to avoid rejecting the whole event on malformed JSON.Use safeParse and fallback; log parse errors without throwing.
Apply:
- const settings = hubSpotSettingsSchema.parse(installation.settings ?? {}); + const parsed = hubSpotSettingsSchema.safeParse(installation.settings ?? {}); + const closedWonDealStageId = + parsed.success ? parsed.data.closedWonDealStageId ?? undefined : undefined; + if (!parsed.success) { + console.warn( + "[HubSpot] Invalid installation.settings shape for portalId", + portalId, + parsed.error?.message, + ); + } @@ - closedWonDealStageId: settings?.closedWonDealStageId, + closedWonDealStageId,apps/web/lib/integrations/hubspot/schema.ts (1)
14-21: Normalize closedWonDealStageId to avoid case/whitespace mismatches.Helps prevent false negatives when comparing to hubspot dealstage.
Apply:
export const hubSpotSettingsSchema = z.object({ - closedWonDealStageId: z - .string() - .nullish() - .describe("The ID of the deal stage that represents a closed won deal."), + closedWonDealStageId: z + .string() + .transform((s) => s.trim().toLowerCase()) + .nullish() + .describe("The ID of the deal stage that represents a closed won deal."), });apps/web/lib/integrations/hubspot/update-contact.ts (2)
1-1: Mark server-only to prevent accidental client bundling of HubSpot access tokens.Apply:
+import "server-only"; import { HUBSPOT_API_HOST } from "./constants";
37-45: Harden error handling (non‑JSON responses) and include status in errors.HubSpot may return text/HTML on errors; response.json() will throw.
Apply:
- const result = await response.json(); + const text = await response.text(); + const result = (() => { + try { + return JSON.parse(text); + } catch { + return text; + } + })(); @@ - if (!response.ok) { - throw new Error(result.message || "Failed to update contact"); - } + if (!response.ok) { + const msg = + typeof result === "string" ? result : result?.message || "Failed to update contact"; + throw new Error(`[HubSpot] ${response.status} ${response.statusText}: ${msg}`); + }apps/web/lib/integrations/hubspot/ui/settings.tsx (4)
17-19: Prefer blank initial value; use placeholder for the default.Pre-filling with the default risks saving it unintentionally.
Apply:
- const [closedWonDealStageId, setClosedWonDealStageId] = useState( - (settings as any)?.closedWonDealStageId || DEFAULT_CLOSED_WON_DEAL_STAGE_ID, - ); + const [closedWonDealStageId, setClosedWonDealStageId] = useState( + (settings as any)?.closedWonDealStageId ?? "", + );
33-41: Normalize input and surface missing workspace feedback.Apply:
- if (!workspaceId) { - return; - } + if (!workspaceId) { + toast.error("No workspace selected."); + return; + } - await executeAsync({ + const normalized = closedWonDealStageId.trim(); + await executeAsync({ workspaceId, - closedWonDealStageId: closedWonDealStageId || null, + closedWonDealStageId: normalized || null, });
51-53: Add a label for accessibility.Apply:
- <p className="text-sm font-medium text-neutral-700"> - Closed Won Deal Stage ID - </p> + <label + htmlFor="closedWonDealStageId" + className="text-sm font-medium text-neutral-700" + > + Closed Won Deal Stage ID + </label>
63-73: Wire input to the label via id.Apply:
- <input + <input + id="closedWonDealStageId" className="w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm" placeholder={`Enter deal stage ID (e.g., ${DEFAULT_CLOSED_WON_DEAL_STAGE_ID})`} type="text" autoComplete="off" name="closedWonDealStageId" value={closedWonDealStageId} onChange={(e) => setClosedWonDealStageId(e.target.value)} />apps/web/lib/integrations/hubspot/track-sale.ts (4)
20-22: Avoid parameter reassignment and normalize expected stage once.Apply:
- closedWonDealStageId = - closedWonDealStageId ?? DEFAULT_CLOSED_WON_DEAL_STAGE_ID; + const expectedClosedWonDealStageId = ( + closedWonDealStageId ?? DEFAULT_CLOSED_WON_DEAL_STAGE_ID + ).trim().toLowerCase();
38-41: Compare normalized dealstage values.Apply:
- if (propertyValue !== closedWonDealStageId) { - console.error( - `[HubSpot] Unknown propertyValue ${propertyValue}. Expected ${closedWonDealStageId}.`, - ); + const actualDealstage = String(propertyValue).trim().toLowerCase(); + if (actualDealstage !== expectedClosedWonDealStageId) { + console.error( + `[HubSpot] Unknown propertyValue ${propertyValue}. Expected ${expectedClosedWonDealStageId}.`, + );
56-60: Validate numeric amount before proceeding.Apply:
if (!properties.amount) { console.error(`[HubSpot] Amount is not set for deal ${dealId}`); return; } + const cents = Math.round(parseFloat(properties.amount) * 100); + if (Number.isNaN(cents)) { + console.error(`[HubSpot] Invalid amount for deal ${dealId}: ${properties.amount}`); + return; + }
80-83: Pass integer cents to trackSale.Apply:
- amount: Number(properties.amount) * 100, + amount: cents,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
apps/web/app/(ee)/api/hubspot/webhook/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page.tsx(2 hunks)apps/web/lib/integrations/hubspot/constants.ts(1 hunks)apps/web/lib/integrations/hubspot/get-contact.ts(1 hunks)apps/web/lib/integrations/hubspot/schema.ts(2 hunks)apps/web/lib/integrations/hubspot/track-lead.ts(5 hunks)apps/web/lib/integrations/hubspot/track-sale.ts(2 hunks)apps/web/lib/integrations/hubspot/types.ts(1 hunks)apps/web/lib/integrations/hubspot/ui/settings.tsx(1 hunks)apps/web/lib/integrations/hubspot/update-contact.ts(1 hunks)apps/web/lib/integrations/hubspot/update-hubspot-settings.ts(1 hunks)apps/web/lib/types.ts(1 hunks)packages/prisma/schema/integration.prisma(1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Only use components exported by hubspot/ui-extensions in settings components
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Do not use React components from hubspot/ui-extensions/crm in settings components
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Do not use window.fetch in settings components; use hubspot.fetch from hubspot/ui-extensions
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Settings components must not access the global window object
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsxapps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-17T02:53:28.359Z
Learnt from: devkiran
PR: dubinc/dub#2839
File: apps/web/lib/integrations/hubspot/schema.ts:5-12
Timestamp: 2025-09-17T02:53:28.359Z
Learning: HubSpot's OAuth token response returns `scopes` as an array of strings, not as a space-delimited string. The schema `scopes: z.array(z.string())` in hubSpotAuthTokenSchema is correct for HubSpot's actual API response format.
Applied to files:
apps/web/lib/integrations/hubspot/types.ts
🔇 Additional comments (16)
apps/web/lib/types.ts (1)
355-384: Adding settings to InstalledIntegrationInfoProps: LGTMExposing
settings?: Prisma.JsonValuealigns with the new InstalledIntegration.settings JSON and downstream UI usage.packages/prisma/schema/integration.prisma (1)
38-39: Persist per-installation settings: LGTMNew
settings Json?on InstalledIntegration fits the flow and keeps credentials/settings split cleanly.apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page-client.tsx (1)
4-4: Wires HubSpot settings UI correctlyImporting
HubSpotSettingsand adding it tointegrationSettingskeyed byHUBSPOT_INTEGRATION_IDlooks good.Minor: import paths for integration IDs vary across files (
@dub/utilsvs@dub/utils/src/...). Consider standardizing to a single public entrypoint to avoid path drift.Also applies to: 60-61
apps/web/lib/integrations/hubspot/types.ts (1)
2-6: Type export for HubSpotContact: LGTMImporting from the schema module and exporting
HubSpotContactkeeps types aligned with validation.Also applies to: 12-12
apps/web/lib/integrations/hubspot/get-contact.ts (1)
13-14: Include additional contact properties: LGTMAdding
dub_linkanddub_partner_emailto the query matches the updated schema and is safe when absent in a portal.apps/web/lib/integrations/hubspot/constants.ts (1)
27-27: Default closed-won stage constant: LGTM
DEFAULT_CLOSED_WON_DEAL_STAGE_ID = "closedwon"provides a sane fallback for default pipelines.apps/web/app/app.dub.co/(dashboard)/[slug]/settings/(basic-layout)/integrations/[integrationSlug]/page.tsx (1)
60-63: Propagate installation settings to client: LGTMPassing
settingsthrough alongsidecredentialsenables the new UI.Note: this page still uses the first installation record. If multiple exist for a workspace, confirm this is intended or adjust the query/ordering.
Also applies to: 89-90
apps/web/lib/integrations/hubspot/update-hubspot-settings.ts (1)
35-47: Guard against writing undefined into a JSON columnOnly include
closedWonDealStageIdwhen defined to avoid Prisma JSON errors.Apply this diff:
data: { - settings: { - ...current, - closedWonDealStageId, - }, + settings: { + ...current, + ...(closedWonDealStageId !== undefined + ? { closedWonDealStageId } + : {}), + }, },If
hubSpotSettingsSchemaguaranteesclosedWonDealStageIdis neverundefined(e.g., string or null), this guard is optional—please confirm.apps/web/lib/integrations/hubspot/schema.ts (1)
37-40: LGTM: contact schema additions.Optional fields and nullish semantics look correct.
apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
4-7: LGTM — no call-site changes requiredtrackHubSpotSaleEvent uses named-args and closedWonDealStageId is optional with a default; the existing call in apps/web/app/(ee)/api/hubspot/webhook/route.ts is fine.
apps/web/lib/integrations/hubspot/track-lead.ts (6)
2-4: Confirm Node runtime when using Prisma + waitUntilIf this handler can run on the Edge runtime, Prisma will break. Ensure this path is Node.js-only (e.g., runtime: "nodejs") or gate Prisma usage accordingly.
44-53: LGTM: sensible defaults for the 0-1 (contact created) flowDeferred tracking with safe name construction and dub_id requirement looks good.
55-63: LGTM: defer HubSpot contact updatewaitUntil usage to decouple post-track updates is appropriate.
99-108: Deal path: normalize name, add externalId fallback, prefer dub_id clickId, and catch errorsPrevents “undefined undefined”, ensures an externalId when email is absent, and avoids crashing the webhook on trackLead errors.
- const trackLeadResult = await trackLead({ - clickId: "", - eventName: `Deal ${properties.dealstage}`, - customerExternalId: contactInfo.properties.email, - customerName: `${contactInfo.properties.firstname} ${contactInfo.properties.lastname}`, - customerEmail: contactInfo.properties.email, - mode: "async", - workspace, - rawBody: payload, - }); + const customerEmail = contactInfo.properties.email || null; + const customerExternalId = customerEmail ?? String(contactInfo.id); + const customerName = + [contactInfo.properties.firstname, contactInfo.properties.lastname] + .filter(Boolean) + .join(" ") || null; + let trackLeadResult: TrackLeadResponse | null = null; + try { + trackLeadResult = await trackLead({ + clickId: contactInfo.properties.dub_id || "", + eventName: `Deal ${properties.dealstage}`, + customerExternalId, + customerName, + customerEmail, + mode: "async", + workspace, + rawBody: payload, + }); + } catch (err) { + console.error(`[HubSpot] trackLead failed for deal ${objectId}:`, err); + return; + }
110-118: LGTM: defers update after deal-trackPost-track waitUntil scheduling is consistent with the contact flow.
124-171: Update only missing HubSpot fields, avoid throw-on-miss, and wrap in try/catch; limit logs to devPrevents overwriting existing values, avoids unhandled rejections from findUniqueOrThrow, and reduces noisy logs.
- if (contact.properties.dub_link && contact.properties.dub_partner_email) { - console.log( - `[HubSpot] Contact ${contact.id} already has dub_link and dub_partner_email. Skipping update.`, - ); - return; - } - - const properties: Record<string, string> = {}; - - if (trackLeadResult.link?.partnerId) { - const partner = await prisma.partner.findUniqueOrThrow({ - where: { - id: trackLeadResult.link.partnerId, - }, - select: { - email: true, - }, - }); - - if (partner.email) { - properties["dub_partner_email"] = partner.email; - } - } - - if (trackLeadResult.link?.url) { - properties["dub_link"] = trackLeadResult.link.url; - } - - if (Object.keys(properties).length === 0) { - return; - } - - await updateHubSpotContact({ - contactId: contact.id, - accessToken, - properties, - }); + try { + if (contact.properties.dub_link && contact.properties.dub_partner_email) { + if (process.env.NODE_ENV === "development") { + console.log( + `[HubSpot] Contact ${contact.id} already has dub_link and dub_partner_email. Skipping update.`, + ); + } + return; + } + + const patch: { dub_partner_email?: string; dub_link?: string } = {}; + + if (trackLeadResult.link?.partnerId && !contact.properties.dub_partner_email) { + const partner = await prisma.partner.findUnique({ + where: { id: trackLeadResult.link.partnerId }, + select: { email: true }, + }); + if (partner?.email) { + patch.dub_partner_email = partner.email; + } + } + + if (trackLeadResult.link?.url && !contact.properties.dub_link) { + patch.dub_link = trackLeadResult.link.url; + } + + if (Object.keys(patch).length === 0) { + return; + } + + await updateHubSpotContact({ + contactId: contact.id, + accessToken, + properties: patch, + }); + } catch (error) { + console.error(`[HubSpot] Failed to update contact ${contact.id}:`, error); + }
Summary by CodeRabbit
New Features
Improvements
Chores