Skip to content

Add UTM parameter support for shortened links#997

Open
seler wants to merge 2 commits intothedevs-network:mainfrom
seler:feat/utm-params
Open

Add UTM parameter support for shortened links#997
seler wants to merge 2 commits intothedevs-network:mainfrom
seler:feat/utm-params

Conversation

@seler
Copy link
Copy Markdown

@seler seler commented Mar 25, 2026

  • Links can now carry UTM parameters (source, medium, campaign, term, content). On redirect, buildTargetURL() appends them to the target URL.
  • Migration adds five nullable string columns to links.
  • All UTM fields are validated as strings, max 255 chars.
  • New input fields in the shortener form and both edit dialogs (user + admin).
image

Allow users to set UTM parameters (source, medium, campaign, term, content)
when creating or editing links. Parameters are appended to the target URL on redirect.
Copilot AI review requested due to automatic review settings March 25, 2026 18:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class UTM parameter support to the link shortener so creators can store campaign metadata on a link and have it automatically appended to the target URL during redirects.

Changes:

  • Adds utm_source, utm_medium, utm_campaign, utm_term, utm_content fields to create/edit UIs and request validation.
  • Persists UTM fields on link creation/edit and appends them to the redirect destination via buildTargetURL().
  • Introduces a DB migration adding 5 nullable UTM columns to the links table and updates link selection/creation queries to include them.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server/views/partials/shortener.hbs Adds UTM inputs to the “advanced options” section of the shortener form.
server/views/partials/links/edit.hbs Adds UTM inputs to the user link edit form.
server/views/partials/admin/links/edit.hbs Adds UTM inputs to the admin link edit form.
server/queries/link.queries.js Includes UTM columns in selectable fields and persists them on link creation.
server/migrations/20260325180240_utm_params.js Adds UTM columns to the links table (and defines a rollback).
server/handlers/validators.handler.js Adds validation/sanitization for all UTM fields on create/edit.
server/handlers/links.handler.js Builds redirect target URL with stored UTMs; wires UTM fields into create/edit + redirect flows.
Comments suppressed due to low confidence (2)

server/handlers/links.handler.js:220

  • In the edit flow, if (!value) { delete req.body[name]; } treats empty strings as “not provided”. That makes it impossible to clear an existing UTM field from a link (submitting an empty input deletes the key, so the subsequent utm_* !== undefined update never runs). Consider handling utm_* (and any other clearable optional fields) explicitly: interpret "" as null and keep the key so it can be persisted, and ensure isChanged is set when the stored value is being cleared.
  ].forEach(([value, name]) => {
    if (!value) {
      if (name === "password" && link.password)
        req.body.password = null;
      else {
        delete req.body[name];
        return;

server/handlers/links.handler.js:325

  • Same issue as the user edit handler: the if (!value) { delete req.body[name]; } logic prevents clearing existing utm_* values (empty input results in the field being removed from req.body, so it won’t be updated to null). Handle empty-string submissions for UTM fields explicitly (e.g., normalize "" to null and keep the key) so admins can remove previously set UTM params.
  ].forEach(([value, name]) => {
    if (!value) {
      if (name === "password" && link.password)
        req.body.password = null;
      else {
        delete req.body[name];
        return;
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/migrations/20260325180240_utm_params.js Outdated
Comment thread server/migrations/20260325180240_utm_params.js Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

server/handlers/links.handler.js:324

  • Same issue as the non-admin edit path: the if (!value) { delete req.body[name]; } block will drop falsy utm_* values, so admins can’t clear existing UTM parameters (and null won’t persist either). Consider normalizing "" to null and allowing explicit nulling for the UTM fields rather than deleting them.
    if (!value) {
      if (name === "password" && link.password)
        req.body.password = null;
      else {
        delete req.body[name];
        return;

server/handlers/links.handler.js:220

  • The edit change-detection logic deletes any falsy field (if (!value) { delete req.body[name]; }). For the new utm_* fields this prevents clearing a previously-set value (empty string / null gets deleted, so the update never writes NULL). If clearing should be supported, handle UTM fields (and any other nullable fields) explicitly by converting "" to null and treating that as a change instead of deleting the key.
    if (!value) {
      if (name === "password" && link.password)
        req.body.password = null;
      else {
        delete req.body[name];
        return;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5 to +9
table.string("utm_source");
table.string("utm_medium");
table.string("utm_campaign");
table.string("utm_term");
table.string("utm_content");
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validators enforce a max length of 255 for UTM fields, but the migration defines them as table.string("utm_*" ) without an explicit length. To keep DB constraints aligned with validation across different DB backends, consider specifying the length explicitly (e.g., table.string("utm_source", 255) etc.).

Suggested change
table.string("utm_source");
table.string("utm_medium");
table.string("utm_campaign");
table.string("utm_term");
table.string("utm_content");
table.string("utm_source", 255);
table.string("utm_medium", 255);
table.string("utm_campaign", 255);
table.string("utm_term", 255);
table.string("utm_content", 255);

Copilot uses AI. Check for mistakes.
module.exports = {
up,
down
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration file formatting is inconsistent with other migrations: module.exports typically ends with }; (see e.g. server/migrations/20250106070444_remove_cooldown.js). Consider adding the trailing semicolon here for consistency.

Suggested change
}
};

Copilot uses AI. Check for mistakes.
Comment on lines +123 to 127
const { reuse, password, customurl, description, target, fetched_domain, expire_in,
utm_source, utm_medium, utm_campaign, utm_term, utm_content } = req.body;
const domain_id = fetched_domain ? fetched_domain.id : null;

const targetDomain = utils.removeWww(URL.parse(target).hostname);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reuse mode currently looks up an existing link by { target, user_id, domain_id } only. With UTM fields now affecting the final redirect URL, this can return an existing link whose stored UTM parameters differ from the ones submitted, so the response/redirect won’t reflect the requested UTM values. Consider including the UTM fields in the reuse lookup (or disabling reuse when any UTM field is present) so “reuse” only triggers when the redirect behavior would be identical.

Copilot uses AI. Check for mistakes.
@seler
Copy link
Copy Markdown
Author

seler commented Mar 27, 2026

Any chance of review from human? :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants