Skip to content

tweak: prevent Gmail from threading data exports#6551

Open
jaskfla wants to merge 6 commits into
devfrom
email-refactors
Open

tweak: prevent Gmail from threading data exports#6551
jaskfla wants to merge 6 commits into
devfrom
email-refactors

Conversation

@jaskfla

@jaskfla jaskfla commented Nov 18, 2025

Copy link
Copy Markdown
Contributor

🦸 Review Hero

  • Run Review Hero
  • Auto-fix review suggestions
  • Auto-fix CI failures

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @jaskfla, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where Gmail would thread multiple data export notification emails, making it difficult to distinguish between individual exports. By introducing a unique identifier in the email headers, each export notification will now appear as a separate conversation. Additionally, the changes include refactoring email utility functions for better modularity and maintainability, along with minor code modernizations and performance optimizations.

Highlights

  • Prevent Gmail Threading for Data Exports: Implemented a mechanism to prevent Gmail from grouping data export emails into a single conversation thread by adding a unique X-Entity-Ref-ID header to each email sent via the emailAfterTimeout utility.
  • Email Options Refactoring: The MailOptions interface has been refactored and exported, now allowing for more flexible email configurations, including the ability to pass custom headers directly to the sendEmail function.
  • Code Modernization and Optimization: Updated parseInt to Number.parseInt for consistency, simplified synchronous file system operations (fs.existsSync, fs.mkdirSync), and optimized user data retrieval by explicitly selecting only necessary fields (email, first_name) when fetching user information.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request effectively addresses the goal of preventing Gmail from threading data export emails by adding a unique X-Entity-Ref-ID header. The implementation is solid, propagating the necessary changes through the email sending logic and updating type definitions accordingly.

I've also noticed and commented on some other improvements:

  • The refactoring to only query for necessary user fields is a great performance optimization.
  • The switch to using an options object for sendResponseAsEmail improves code clarity.
  • A bug involving incorrect await usage with synchronous fs methods in ExportSurveyDataHandler.js has been fixed. I've added suggestions to further improve that piece of logic for better performance and robustness by using asynchronous file system APIs.

Overall, these are good changes that improve the codebase. My review comments focus on taking one of the fixes a step further to align with best practices for asynchronous Node.js applications.

Comment thread packages/web-config-server/src/export/ExportSurveyDataHandler.js Outdated
Comment thread packages/web-config-server/src/export/ExportSurveyDataHandler.js Outdated
Comment thread packages/web-config-server/src/export/ExportSurveyDataHandler.js Outdated
@jaskfla jaskfla force-pushed the email-refactors branch 2 times, most recently from afe4beb to 09ed23d Compare November 19, 2025 21:11
Comment thread packages/server-boilerplate/src/utils/emailAfterTimeout.ts Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tip

Review this PR commit-by-commit

await sendEmail(user.email, {
// Prevent threading in Gmail, even if subject is the same. (`emailAfterTimeout` is used for
// large data exports; grouping separate exports into a single thread is probably undesirable.)
headers: { 'X-Entity-Ref-ID': crypto.randomUUID() },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Bugs & Correctness] suggestion

crypto is used as a global without an explicit import. The Web Crypto API (globalThis.crypto) is only available as an unflagged global from Node.js v19+. While .node-version is set to latest, relying on the implicit global is fragile — an explicit import { randomUUID } from 'node:crypto' (and calling randomUUID() directly) makes the intent clear and is compatible with any Node.js version that supports randomUUID (v14.17.0+).


[Tupaia Conventions] suggestion

crypto.randomUUID() uses the global crypto object, which was only added as a Node.js global in v19. For older Node versions this will throw at runtime. Prefer an explicit import: import { randomUUID } from 'node:crypto'. This is consistent with how web-config-server now uses import fs from 'node:fs/promises' (explicit node: protocol imports) in the same PR.

req: any,
res: any,
constructEmailFromResponse: ConstructEmailFromResponseT,
constructEmailFromResponse: ConstructEmailFromResponseT<typeof req>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Tupaia Conventions] suggestion

ConstructEmailFromResponseT<typeof req> where req: anytypeof req resolves to any, making the generic parameter meaningless and providing no additional type safety. Since req and res are already any in setupEmailResponse, the generic annotation is misleading. Either type req properly as Request throughout the function, or drop the generic and use ConstructEmailFromResponseT directly (matching its default T = unknown).

(req: any, res: any, next: () => void) => {
const { respondWithEmailTimeout } = req.query;
(req: Request, res: Response, next: NextFunction) => {
const { respondWithEmailTimeout } = req.query as { respondWithEmailTimeout?: string };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Bugs & Correctness] suggestion

The type cast req.query as { respondWithEmailTimeout?: string } silently hides the case where the query parameter is supplied multiple times (e.g. ?respondWithEmailTimeout=5&respondWithEmailTimeout=10), which Express parses as string[]. Number.parseInt coerces an array to a string via .toString(), so ['5','10'] becomes '5,10' and parseInt silently returns 5, ignoring the second value without any error. A simple guard like const raw = req.query.respondWithEmailTimeout; if (Array.isArray(raw)) throw new Error(...) would prevent this silent mis-parse.

@review-hero

review-hero Bot commented Mar 15, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
4 agents reviewed this PR | 0 critical | 4 suggestions | 0 nitpicks

Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/server-boilerplate/src/utils/emailAfterTimeout.ts:21: crypto is used as a global without an explicit import. The Web Crypto API (globalThis.crypto) is only available as an unflagged global from Node.js v19+. While .node-version is set to latest, relying on the implicit global is fragile — an explicit import { randomUUID } from 'node:crypto' (and calling randomUUID() directly) makes the intent clear and is compatible with any Node.js version that supports randomUUID (v14.17.0+).

crypto.randomUUID() uses the global crypto object, which was only added as a Node.js global in v19. For older Node versions this will throw at runtime. Prefer an explicit import: import { randomUUID } from 'node:crypto'. This is consistent with how web-config-server now uses import fs from 'node:fs/promises' (explicit node: protocol imports) in the same PR.


packages/server-boilerplate/src/utils/emailAfterTimeout.ts:35: ConstructEmailFromResponseT&lt;typeof req> where req: anytypeof req resolves to any, making the generic parameter meaningless and providing no additional type safety. Since req and res are already any in setupEmailResponse, the generic annotation is misleading. Either type req properly as Request throughout the function, or drop the generic and use ConstructEmailFromResponseT directly (matching its default T = unknown).


packages/server-boilerplate/src/utils/emailAfterTimeout.ts:66: The type cast req.query as { respondWithEmailTimeout?: string } silently hides the case where the query parameter is supplied multiple times (e.g. ?respondWithEmailTimeout=5&respondWithEmailTimeout=10), which Express parses as string[]. Number.parseInt coerces an array to a string via .toString(), so ['5','10'] becomes '5,10' and parseInt silently returns 5, ignoring the second value without any error. A simple guard like const raw = req.query.respondWithEmailTimeout; if (Array.isArray(raw)) throw new Error(...) would prevent this silent mis-parse.

@jaskfla jaskfla force-pushed the email-refactors branch 2 times, most recently from 841a5f8 to 4363212 Compare April 14, 2026 02:41
req: any,
res: any,
constructEmailFromResponse: ConstructEmailFromResponseT,
constructEmailFromResponse: ConstructEmailFromResponseT<typeof req>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Generic type parameter bound to wrong variable

Low Severity

The generic T in ConstructEmailFromResponseT<T> types the responseBody parameter, but setupEmailResponse instantiates it as ConstructEmailFromResponseT<typeof req>, which semantically says "the response body has the same type as the request." This is incorrect — the response body and request are unrelated types. Currently harmless because req: any makes typeof req resolve to any, but if req is ever properly typed, this would incorrectly constrain responseBody to the request type.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4363212. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5a93702. Configure here.

};
};
interface EmailAfterTimeoutMailOptions
extends Pick<MailOptions, 'attachments' | 'subject' | 'templateContext'> {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Email subject became optional weakening type safety

Low Severity

EmailAfterTimeoutMailOptions picks subject from MailOptions, which inherits it from nodemailer's Mail.Options where it's optional (subject?: string). The previous code explicitly required subject: string in the ConstructEmailFromResponseT return type. Future implementations of constructEmailFromResponse can now omit the subject without a TypeScript error, potentially resulting in emails sent without a subject line.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5a93702. Configure here.

@jaskfla jaskfla force-pushed the email-refactors branch from 5a93702 to aa2a07f Compare May 29, 2026 02:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant