Conversation
|
@bensabic is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
This PR adds a Dub plugin for creating and managing short links with two main actions: Create Link and Upsert Link. The implementation includes proper credential management, error handling, and grouped configuration for Link IDs, Link Preview metadata, and UTM parameters.
- Adds complete Dub integration with API key authentication
- Implements two actions: Create Link (POST) and Upsert Link (PUT) with comprehensive input fields
- Returns structured link data including short URL, QR code, and metadata
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/index.ts | Registers the new Dub plugin in the plugins index |
| plugins/dub/index.ts | Main plugin configuration defining actions, form fields, and test config |
| plugins/dub/credentials.ts | Type definition for Dub API credentials |
| plugins/dub/icon.tsx | SVG icon component for Dub branding |
| plugins/dub/test.ts | Connection test function to validate API credentials |
| plugins/dub/steps/create-link.ts | Step handler for creating new short links via POST |
| plugins/dub/steps/upsert-link.ts | Step handler for creating or updating links via PUT |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function stepHandler( | ||
| input: CreateLinkCoreInput, | ||
| credentials: DubCredentials | ||
| ): Promise<CreateLinkResult> { | ||
| const apiKey = credentials.DUB_API_KEY; | ||
|
|
||
| if (!apiKey) { | ||
| return { | ||
| success: false, | ||
| error: | ||
| "DUB_API_KEY is not configured. Please add it in Project Integrations.", | ||
| }; | ||
| } | ||
|
|
||
| if (!input.url) { | ||
| return { | ||
| success: false, | ||
| error: "Destination URL is required", | ||
| }; | ||
| } | ||
|
|
||
| try { | ||
| const body: Record<string, string> = { | ||
| url: input.url, | ||
| }; | ||
|
|
||
| if (input.key) body.key = input.key; | ||
| if (input.domain) body.domain = input.domain; | ||
| if (input.externalId) body.externalId = input.externalId; | ||
| if (input.tenantId) body.tenantId = input.tenantId; | ||
| if (input.programId) body.programId = input.programId; | ||
| if (input.partnerId) body.partnerId = input.partnerId; | ||
| if (input.title) body.title = input.title; | ||
| if (input.description) body.description = input.description; | ||
| if (input.image) body.image = input.image; | ||
| if (input.video) body.video = input.video; | ||
| if (input.utm_source) body.utm_source = input.utm_source; | ||
| if (input.utm_medium) body.utm_medium = input.utm_medium; | ||
| if (input.utm_campaign) body.utm_campaign = input.utm_campaign; | ||
| if (input.utm_term) body.utm_term = input.utm_term; | ||
| if (input.utm_content) body.utm_content = input.utm_content; | ||
|
|
||
| const response = await fetch(`${DUB_API_URL}/links`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${apiKey}`, | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = (await response.json().catch(() => ({}))) as { | ||
| error?: { message?: string }; | ||
| message?: string; | ||
| }; | ||
| const errorMessage = | ||
| errorData.error?.message || errorData.message || `HTTP ${response.status}`; | ||
| return { | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
|
|
||
| const link = (await response.json()) as DubLinkResponse; | ||
|
|
||
| return { | ||
| success: true, | ||
| id: link.id, | ||
| shortLink: link.shortLink, | ||
| qrCode: link.qrCode, | ||
| domain: link.domain, | ||
| key: link.key, | ||
| url: link.url, | ||
| }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: `Failed to create link: ${getErrorMessage(error)}`, | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
The stepHandler function in this file and plugins/dub/steps/upsert-link.ts share almost identical logic (lines 55-136 here vs lines 55-136 there). The only differences are the API endpoint, HTTP method, and error message. Consider extracting the shared logic into a common utility function to reduce code duplication and improve maintainability. For example, create a shared function that accepts the endpoint, method, and operation name as parameters.
| configFields: [ | ||
| { | ||
| key: "url", | ||
| label: "Destination URL", | ||
| type: "template-input", | ||
| placeholder: "https://example.com/page", | ||
| example: "https://example.com/landing-page", | ||
| required: true, | ||
| }, | ||
| { | ||
| key: "key", | ||
| label: "Custom Slug", | ||
| type: "template-input", | ||
| placeholder: "my-link", | ||
| example: "summer-sale", | ||
| }, | ||
| { | ||
| key: "domain", | ||
| label: "Domain", | ||
| type: "template-input", | ||
| placeholder: "dub.sh", | ||
| example: "dub.sh", | ||
| }, | ||
| { | ||
| label: "Link IDs", | ||
| type: "group", | ||
| fields: [ | ||
| { | ||
| key: "externalId", | ||
| label: "External ID", | ||
| type: "template-input", | ||
| placeholder: "my-external-id", | ||
| }, | ||
| { | ||
| key: "tenantId", | ||
| label: "Tenant ID", | ||
| type: "template-input", | ||
| placeholder: "tenant-123", | ||
| }, | ||
| { | ||
| key: "programId", | ||
| label: "Program ID", | ||
| type: "template-input", | ||
| placeholder: "program-123", | ||
| }, | ||
| { | ||
| key: "partnerId", | ||
| label: "Partner ID", | ||
| type: "template-input", | ||
| placeholder: "partner-123", | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| label: "Link Preview", | ||
| type: "group", | ||
| fields: [ | ||
| { | ||
| key: "title", | ||
| label: "Title", | ||
| type: "template-input", | ||
| placeholder: "Custom preview title", | ||
| }, | ||
| { | ||
| key: "description", | ||
| label: "Description", | ||
| type: "template-input", | ||
| placeholder: "Custom preview description", | ||
| }, | ||
| { | ||
| key: "image", | ||
| label: "Image URL", | ||
| type: "template-input", | ||
| placeholder: "https://example.com/image.png", | ||
| }, | ||
| { | ||
| key: "video", | ||
| label: "Video URL", | ||
| type: "template-input", | ||
| placeholder: "https://example.com/video.mp4", | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| label: "UTM Parameters", | ||
| type: "group", | ||
| fields: [ | ||
| { | ||
| key: "utm_source", | ||
| label: "Source", | ||
| type: "template-input", | ||
| placeholder: "newsletter", | ||
| }, | ||
| { | ||
| key: "utm_medium", | ||
| label: "Medium", | ||
| type: "template-input", | ||
| placeholder: "email", | ||
| }, | ||
| { | ||
| key: "utm_campaign", | ||
| label: "Campaign", | ||
| type: "template-input", | ||
| placeholder: "summer-sale", | ||
| }, | ||
| { | ||
| key: "utm_term", | ||
| label: "Term", | ||
| type: "template-input", | ||
| placeholder: "running+shoes", | ||
| }, | ||
| { | ||
| key: "utm_content", | ||
| label: "Content", | ||
| type: "template-input", | ||
| placeholder: "logolink", | ||
| }, | ||
| ], | ||
| }, | ||
| ], |
There was a problem hiding this comment.
The configFields array for "create-link" (lines 51-170) and "upsert-link" (lines 187-306) are identical. Consider extracting this configuration into a shared constant to reduce duplication and ensure consistency. For example: const linkConfigFields = [...] defined once and reused for both actions.
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <title>Dub</title> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="currentColor"/> |
There was a problem hiding this comment.
SVG attributes should use React/JSX camelCase naming convention. Change fill-rule to fillRule and clip-rule to clipRule to match React's JSX syntax and be consistent with other icon components in the codebase (see plugins/fal/icon.tsx and plugins/slack/icon.tsx).
| <path fill-rule="evenodd" clip-rule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="currentColor"/> | |
| <path fillRule="evenodd" clipRule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="currentColor"/> |
Summary
Adds Dub plugin with the following actions:
Features:
Test plan