Skip to content

Add org logo, treated links, and first-view email to transactional emails - #2399

Merged
steve8708 merged 30 commits into
mainfrom
ai_main_bc3e0b95875944d98162
Jul 28, 2026
Merged

Add org logo, treated links, and first-view email to transactional emails#2399
steve8708 merged 30 commits into
mainfrom
ai_main_bc3e0b95875944d98162

Conversation

@timmilazzo

@timmilazzo timmilazzo commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the shared email template and sending infrastructure with organization branding, treated link blocks, and custom sender/reply-to support, and wires up a new "first view" transactional email that fires when a recording's owner receives its first genuine non-owner view.

Problem

The existing transactional email system only supported a fixed Agent Native logo, plain CTA buttons, and a single verified sender address/name. There was also no notification sent to a recording owner when their Clip was first viewed by someone else, and no way to safely surface a copyable URL (e.g. for feeding a link to an AI agent) inside an email.

Solution

Enhanced renderEmail to accept an optional organization logo and a "link block" that renders a safe, escaped, copyable URL either before or after the CTA in both HTML and plain-text output. Extended sendEmail to allow overriding the sender display name while keeping the verified email address, and to map a custom Reply-To. Added logic in the view-event API route to detect the first time a recording transitions to a "counted" view by a non-owner, guard against duplicates/historical views predating email-enablement, and enqueue a first-view transactional email to the owner.

Key Changes

  • email-template.ts: added brandLogoUrl (falls back to the existing cid: embedded logo) and linkBlock (intro, url, placement: "before-cta" | "after-cta") options, rendered safely in both HTML and text output.
  • email.ts: added fromName to SendEmailArgs, allowing the display name on the from field to be overridden while preserving the verified sender address; Reply-To mapping confirmed via new test coverage.
  • core/package.json: exposed a new ./user-profile/server subpath export pointing to the user-profile store, for server-side consumption (e.g. resolving org logos/profile names for emails).
  • templates/clips/server/routes/api/view-event.post.ts: on transition to a counted view, checks whether the current viewer/session is genuinely the first non-owner view (post enablement date) and enqueues a first-view transactional email to the recording owner via transactionalEmailStore; uses ownerEmailMatches for case-insensitive owner comparison and skips enqueue for owner-initiated or already-counted views.
  • Added/updated tests in email-template.spec.ts, email.spec.ts, and view-event.post.spec.ts covering org logo fallback, link block placement/escaping, sender name/reply-to overrides, and various first-view enqueue scenarios (anonymous transition, already-counted, owner view, historical view predating enablement, genuine first view).

Edit in Builder  Preview


To clone this PR locally use the Github CLI with command gh pr checkout 2399

You can tag me at @BuilderIO for anything you want me to fix or change

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@builder-io-integration builder-io-integration Bot changed the title Update from the Builder.io agent Add org logo, treated links, and first-view email to transactional emails Jul 25, 2026
@netlify

This comment has been minimized.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 4 potential issues 🔴

Review Details

Code Review Summary

PR #2399 expands the shared transactional-email surface with organization branding, safe link blocks, sender-name overrides, and a new first-view notification triggered from Clips view tracking. It also adds the /import Clips flow and related navigation/regression coverage. The email escaping and logical-key deduplication are thoughtful, and the focused tests/builds reported by the agents passed.

Risk: Standard, because this combines user-facing workflow changes with durable business-side effects and background email processing.

Key Findings

  • 🔴 HIGH — The transactional queue defaults to process-local filesystem storage, which is not durable or shared across the supported Netlify/serverless deployment model. Enqueued emails can disappear between invocations, and separate instances cannot coordinate leases.
  • 🟡 MEDIUM — A two-minute sending lease can expire while an unbounded provider request is still running, allowing another worker to reclaim and send the same email, causing duplicates.
  • 🟡 MEDIUM — A crash after creating a .takeover marker can leave a permanent lock that prevents future operations for that logical key.
  • 🟡 MEDIUM — The new public @agent-native/core server export/API has no changeset, so the release workflow may omit the package version/publication update.

🧪 Browser testing: Will run after this review (PR touches UI code); the planner attempted a full run, but all cases were blocked because browser automation tools were unavailable in the executor environment.

Comment on lines +229 to +231
const root =
options.root ??
path.join(process.cwd(), "data", "clips-transactional-emails");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Persist the hosted email queue in durable shared storage

The queue, claims, leases, and reconciliation cursors default to process.cwd()/data, which is process-local and non-durable in the supported Netlify/serverless deployment. An event handler can enqueue on one instance while a worker or later request reads another empty filesystem, so first-view/import/reminder emails can be lost; use the shared database or an explicitly configured durable queue for hosted mode.

Additional Info
Reported independently by 2 review agents.

Fix in Builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not changed in this PR. I agree this is a real limitation for multi-instance/serverless hosting, but the feature was explicitly designed and approved around the repository-backed file queue. Moving jobs, claims, leases, and all reconciliation cursors to shared SQL is a persistence migration, not a safe localized review fix; a partial hosted-mode shim would make the guarantees less clear. This should be tracked and implemented as a dedicated queue-storage migration before enabling these jobs on horizontally scaled hosted deployments.

Comment thread templates/clips/server/jobs/transactional-emails.ts
Comment thread templates/clips/server/lib/transactional-email-store.ts Outdated
Comment thread packages/core/package.json
@steve8708

Copy link
Copy Markdown
Contributor

@builderio-bot look at latest PR feedback and fix anything you agree with. Be skeptical. Reply to every comment (directly on the comment thread of each comment) if you fixed it or not and why. then check back every 2 minutes on a loop and see if any new feedback posted, until at least 20 minutes go by without any new feedback posted we want to address, including making sure CI passes too and no merge conflicts (make sure code is mergeable)

@builder-io-integration

Copy link
Copy Markdown
Contributor

Addressed all PR feedback and replied directly to every thread:

  • Fixed provider lease risk with a 60-second send timeout.
  • Fixed stale .takeover lock recovery with regression coverage.
  • Added the required @agent-native/core changeset.
  • Declined the durable SQL queue migration as out of scope for the explicitly approved file-backed architecture, with rationale.
  • Resolved record.tsx; no local conflicts remain.
  • Full Clips tests/build, 48 focused tests, core build, typecheck, and changeset validation pass.
  • Monitored every two minutes for over 20 minutes; no new feedback or failed checks appeared.

GitHub still evaluates the old remote commit as conflicting with Netlify checks pending. Push the resolved changes using the top-right Push button; CI and remote mergeability can only update afterward.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Here's a visual recap of what changed:

Visual recap

Open the full interactive recap

builder-io-integration[bot]

This comment was marked as outdated.

@steve8708

Copy link
Copy Markdown
Contributor

@builderio-bot please /babysit-pr and auto fix what you agree with

@gitguardian

gitguardian Bot commented Jul 25, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
35177285 Triggered Generic High Entropy Secret fce183c packages/docs/app/routes/_index.tsx View secret
35177286 Triggered Generic High Entropy Secret fce183c packages/docs/app/routes/_index.tsx View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@builder-io-integration

Copy link
Copy Markdown
Contributor

Resolved both merge conflicts and addressed all PR feedback, including replying to the latest false-positive review. Verification passes: 879/879 Clips tests, typecheck, production build, formatting, and both previously failing security guards.

GitHub still reports CONFLICTING because the resolved merge is staged locally but cannot be committed/pushed from this environment. Use the top-right Push/Create PR control to publish the resolution; CI will then rerun.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 3 potential issues 🟡

Review Details

Incremental Code Review Summary

The latest head fixes the previously reported undefined parseSendGridFrom runtime defect; the helper now exists and the corresponding review thread was resolved. The hosted queue durability and provider-send lease-expiry concerns remain open from earlier reviews and were not reposted. The latest changes add three new issues across the Clips UI and transactional/import flows.

Risk: Standard, because this combines user-facing navigation with background email dispatch and media-format handling.

Key Findings

  • 🟡 MEDIUM — The new “Upload video” links point to plain /record rather than /record?autoUpload=1, so they open the normal recording screen instead of the advertised upload picker.
  • 🟡 MEDIUM — Two-Clip AI summary jobs are marked ai_dispatched before browser handoff and stale claims are only logged, so a closed tab or failed handoff can permanently strand the job.
  • 🟡 MEDIUM — Direct WebM/MOV/M4V imports are persisted as MP4, causing downstream playback, transcription, and export code to use the wrong decoder/extension.

🧪 Browser testing: Will run after this review (PR touches UI code); a full run was attempted, but all cases were blocked because browser automation tools were unavailable in the executor environment.

Comment thread templates/clips/app/components/library/library-grid.tsx Outdated
Comment thread templates/clips/actions/list-transactional-email-ai-requests.ts Outdated
oembed.thumbnail_url ?? existingRecording?.thumbnailUrl ?? null,
oembed?.thumbnail_url ?? existingRecording?.thumbnailUrl ?? null,
durationMs,
videoFormat: "mp4" as const,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Preserve the actual format of direct video imports

The direct-link path accepts WebM, MOV, and M4V and preserves the fetched MIME type, but this recording is always saved with videoFormat: "mp4" and later uploaded with an .mp4 name. Downstream playback, transcript fallback, and ffmpeg export select behavior from videoFormat, so non-MP4 bytes can be decoded with the wrong format. Derive a supported format/filename from the MIME type or normalize the media before storing it as MP4.

Additional Info
Found by 1 of 2 review agents; verified against direct-video MIME detection and downstream format-dependent code.

Fix in Builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed locally: direct WebM imports now persist videoFormat: "webm" and upload with a .webm filename; MP4 remains MP4. Because the recording schema/player contract only supports MP4/WebM, the importer now rejects MOV/M4V instead of mislabeling those containers as MP4. Added a WebM regression test.

@steve8708

Copy link
Copy Markdown
Contributor

@builderio-bot CI still failing and more feedback - run the /babysit-pr skill and keep watching and fixing pm a loop until the PR is full green (no new feedback or CI dailures) for at least 30 minutes

@builder-io-integration

Copy link
Copy Markdown
Contributor

Fixed all new feedback and CI issues; local verification is green with 882 tests, production build, formatting, typecheck, all 32 guards, and zero unaddressed threads. The staged fixes and merge commits must be published via the top-right Push control before GitHub CI and the requested 30-minute green soak can begin.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 2 potential issues 🟡

Review Details

Incremental Code Review Summary

The latest head fixes two previously reported issues: the library-grid Upload video link now adds autoUpload=1, and stale ai_dispatched jobs can be reclaimed by a later claimant. The provider-send lease concern is also addressed by the 60-second send timeout relative to the two-minute lease, so that thread was resolved. The hosted queue durability concern remains open and was not reposted.

The latest review found two new issues. The sidebar Upload video controls were not updated along with the grid control, and the narrowed direct-video URL guessing still accepts unsupported video/* response MIME types that the importer relabels as MP4.

Risk: Standard, because these changes affect user-facing recording workflows and persisted media semantics.

Key Findings

  • 🟡 MEDIUM — Both expanded and collapsed sidebar Upload video controls still navigate to plain /record, so they do not trigger the upload picker.
  • 🟡 MEDIUMvideo/quicktime, video/x-m4v, and other unsupported video/* responses are accepted and then persisted as MP4 despite the Clips format contract supporting only WebM and MP4.

🧪 Browser testing: Will run after this review (PR touches UI code); a full run was attempted, but all cases were blocked because browser automation tools were unavailable in the executor environment.

variant="ghost"
asChild
>
<NavLink to="/record">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Make sidebar Upload video controls request auto-upload

Both sidebar Upload video controls still navigate to plain /record, while the recorder only opens the file picker when autoUpload=1 is present. They therefore behave like “New recording” instead of performing the advertised upload action. Use an upload href with autoUpload=1 for both expanded and collapsed sidebar entries, preserving any relevant context.

Additional Info
Found by 1 of 2 review agents and corroborated by repeated browser-planner source inspection. This is distinct from the resolved library-grid link issue.

Fix in Builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed locally: both collapsed and expanded sidebar “Upload video” controls now navigate to /record?autoUpload=1; the separate New recording controls remain on plain /record. Typecheck, formatting, and affected UI tests pass.

Comment on lines +37 to +39
const mimeType =
(headerValue ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
if (mimeType.startsWith("video/")) return mimeType;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Reject unsupported direct-video MIME types

The latest URL-guessing change still accepts every video/* response type. A direct response with video/quicktime or video/x-m4v therefore reaches the importer, which maps every non-WebM MIME type to videoFormat: "mp4" and uploads it as an .mp4 recording. Restrict accepted response types to video/mp4 and video/webm, or add full container support before persisting these formats.

Additional Info
Found by 1 of 2 review agents and verified against import-loom-recording.ts format mapping and the schema/player contract.

Fix in Builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed locally: normalizeDirectVideoMimeType now accepts only exact video/mp4 and video/webm response types (or infers those two from an octet-stream URL). Other video/* types such as QuickTime/M4V are rejected before upload, so they cannot be mislabeled as MP4.

@steve8708

Copy link
Copy Markdown
Contributor

@builderio-bot CI still failing and more feedback - run the /babysit-pr skill and keep watching and fixing pm a loop until the PR is full green (no new feedback or CI dailures) for at least 30 minutes

@builder-io-integration

Copy link
Copy Markdown
Contributor

Resolved the AGENTS.md conflict and both new review findings; formatting, typecheck, affected tests, production build, and all 35 guards pass with zero unaddressed threads. GitHub remains CONFLICTING until the staged main merge is published via the top-right Push control; the 30-minute green soak cannot start before that.

@steve8708

Copy link
Copy Markdown
Contributor

@builderio-bot resolve the merge conflicts plz

@builder-io-integration

Copy link
Copy Markdown
Contributor

Resolved and staged all three merge conflicts, preserving main’s sanitized logo/hero and sender-name changes alongside the PR’s link-block behavior.

Verification passed: 16 email tests, core typecheck, formatting, relevant guards, and Git confirms no unmerged files.

@steve8708
steve8708 merged commit 363d36f into main Jul 28, 2026
100 checks passed
@steve8708
steve8708 deleted the ai_main_bc3e0b95875944d98162 branch July 28, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants