Skip to content

feat(campaigns): implement Sandbox Mode for safe campaign testing (#397) - #690

Open
SurinderTech wants to merge 2 commits into
Kuldeeep18:mainfrom
SurinderTech:main
Open

feat(campaigns): implement Sandbox Mode for safe campaign testing (#397)#690
SurinderTech wants to merge 2 commits into
Kuldeeep18:mainfrom
SurinderTech:main

Conversation

@SurinderTech

@SurinderTech SurinderTech commented Jul 19, 2026

Copy link
Copy Markdown

Pull Request

Closes #397

Summary

This PR implements Sandbox Mode for campaign testing, enabling users to safely test email campaigns without sending emails to actual recipients.

Changes Made

  • Added support for enabling/disabling Sandbox Mode.
  • Added configuration for a dedicated sandbox email address.
  • Updated email sending logic to redirect all campaign emails to the configured sandbox recipient when Sandbox Mode is enabled.
  • Preserved AI-generated email personalization during sandbox testing.
  • Added UI support for Sandbox Mode, including configuration options and status indicators.
  • Updated campaign settings and related backend logic to support the new functionality.
  • Added the required database migration.

Testing

  • Verified emails are redirected to the configured sandbox email.
  • Verified AI personalization is retained in sandbox emails.
  • Verified normal campaign delivery remains unchanged when Sandbox Mode is disabled.
  • Tested both backend functionality and frontend behavior.

Related Issue

Closes #397

Summary by CodeRabbit

  • New Features
    • Added Sandbox Mode for safely testing campaign emails with configurable test inboxes and clear test-delivery warnings.
    • Added Canvas and Flowchart views for campaign sequences, with navigation back to individual steps.
    • Campaigns now track their creator for improved test-delivery routing.
    • Improved AI email personalization with organization-specific API keys, a newer model, and request timeouts.
  • Bug Fixes
    • Improved handling and retry behavior when Sandbox Mode lacks a valid recipient.
    • Cleaned up dashboard document structure.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Campaigns now record their creator and support Sandbox Mode email redirection. The campaign builder adds sandbox controls, launch confirmation, and Canvas/Mermaid Flowchart views. Gemini personalization gains organization-key selection, request timeouts, and diagnostics.

Changes

Sandbox Mode

Layer / File(s) Summary
Campaign creator ownership
backend/campaigns/models.py, backend/campaigns/migrations/..., backend/campaigns/views.py
Campaigns gain an optional created_by user relation, populated during creation and used for sandbox recipient resolution.
Sandbox email delivery
backend/campaigns/tasks.py
Sandbox settings redirect Gmail and SMTP delivery to an override, campaign creator, or connected-account email; unresolved destinations are delayed for retry.
Builder sandbox and flowchart UI
frontend/campaign-builder.html
Campaign settings persist and restore Sandbox Mode, display warnings and launch confirmation, and support Canvas/Mermaid Flowchart switching with step navigation.

Ancillary updates

Layer / File(s) Summary
Gemini model diagnostics
backend/campaigns/ai.py
Personalization selects an organization-level key, uses Gemini 2.5 Flash with a 30-second timeout, and adds request lifecycle logging.
Repository and markup cleanup
.gitignore, backend/campaigns/models.py, backend/campaigns/tasks.py, backend/campaigns/views.py, frontend/dashboard.html
Virtual environment paths are ignored and incidental markup, separator, and unchanged return-text edits are applied.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CampaignBuilder
  participant CampaignViewSet
  participant send_email_step
  participant EmailProvider
  CampaignBuilder->>CampaignViewSet: save sandbox_mode and sandbox_email
  CampaignViewSet->>send_email_step: provide campaign creator
  send_email_step->>send_email_step: resolve sandbox recipient
  send_email_step->>EmailProvider: send redirected email
Loading

Possibly related PRs

Suggested labels: type:feature, frontend, level:advanced

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes like the Mermaid flowchart editor, Gemini API/key logging updates, dashboard cleanup, and .gitignore edits are unrelated to Sandbox Mode. Split unrelated UI, AI, and housekeeping edits into separate PRs, keeping this one focused on Sandbox Mode settings and delivery redirection.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately highlights the main Sandbox Mode campaign testing change.
Linked Issues check ✅ Passed The PR implements campaign-level Sandbox Mode, redirects sends, prefixes subjects, and adds settings/migration support as requested in #397.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/campaigns/ai.py (1)

223-228: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not gate personalization on the global API key.

When GEMINI_API_KEY is empty but lead.organization.gemini_api_key is configured, this early return executes before the organization key is resolved at Lines 255-258. Tenant-specific personalization is therefore silently disabled. Resolve final_api_key first, then log/check that effective key with logger.debug() instead of print().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/ai.py` around lines 223 - 228, Update the personalization
flow around _get_gemini_api_key so it resolves the effective final_api_key,
including lead.organization.gemini_api_key, before applying the missing-key
early return. Replace the API-key print with logger.debug() and log whether the
effective key exists; gate personalization on final_api_key rather than only the
global key.
🧹 Nitpick comments (1)
backend/campaigns/ai.py (1)

247-264: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Migrate this Gemini integration to google-genai. requirements.txt still pins google-generativeai, and this path still uses genai.configure()/GenerativeModel; update the dependency and call sites together so the SDK and model usage stay aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/ai.py` around lines 247 - 264, Replace the
google.generativeai integration in this flow with the google-genai SDK, updating
requirements.txt and the corresponding import and model initialization around
the API-key selection logic. Remove genai.configure() and GenerativeModel usage,
and use the google-genai client/model invocation API consistently while
preserving the existing key precedence and gemini-2.5-flash model.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/campaigns/ai.py`:
- Around line 261-266: Update the Gemini request in personalize_email,
specifically the model.generate_content call, to pass
google.generativeai.types.RequestOptions with a finite timeout such as 30
seconds. Preserve the existing prompt and model selection while ensuring
send_email_step cannot remain blocked by an unbounded request.

In `@backend/campaigns/tasks.py`:
- Around line 544-577: Update _resolve_sandbox_recipient so the explicit
sandbox_email value from campaign.settings is validated and returned before
checking campaign.created_by.email. Preserve the creator email as the fallback
default, followed by the connected account email and None when no destination is
available.
- Around line 544-577: Extend the CampaignLead lookup that loads clead to
select_related campaign__created_by and
campaign__connected_account__connected_by in addition to the existing relations.
Ensure _resolve_sandbox_recipient can access both campaign.created_by.email and
the fallback connected_by.email without additional database queries.

In `@frontend/campaign-builder.html`:
- Around line 1318-1325: Update the placeholder and helper text in the sandbox
email field identified by setting-sandbox-email and sandbox-email-wrap so they
accurately describe the backend’s _resolve_sandbox_recipient fallback behavior,
specifically that the campaign creator’s email is used before the
connected-account owner. Keep the optional override behavior unchanged.
- Around line 1887-1989: The Mermaid flowchart currently interpolates
unsanitized step text into labels while loose security permits HTML execution.
Update escapeMermaidLabel and the renderFlowchart click handling to prevent
user-controlled labels from becoming HTML, preferably by disabling Mermaid click
callbacks and using a safe encoded label representation while preserving
flowchart rendering and step editing through a non-Mermaid interaction.

In `@frontend/dashboard.html`:
- Around line 513-525: Update new-campaign initialization in
frontend/campaign-builder.html to read SANDBOX_DEFAULT_KEY
(leadorbit_default_sandbox_mode) and apply its saved value to the campaign’s
initial sandbox state. Reuse the existing toggle/state initialization symbols
there, or remove initSandboxBanner and its UI if no consumer can be added.

---

Outside diff comments:
In `@backend/campaigns/ai.py`:
- Around line 223-228: Update the personalization flow around
_get_gemini_api_key so it resolves the effective final_api_key, including
lead.organization.gemini_api_key, before applying the missing-key early return.
Replace the API-key print with logger.debug() and log whether the effective key
exists; gate personalization on final_api_key rather than only the global key.

---

Nitpick comments:
In `@backend/campaigns/ai.py`:
- Around line 247-264: Replace the google.generativeai integration in this flow
with the google-genai SDK, updating requirements.txt and the corresponding
import and model initialization around the API-key selection logic. Remove
genai.configure() and GenerativeModel usage, and use the google-genai
client/model invocation API consistently while preserving the existing key
precedence and gemini-2.5-flash model.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb2072c9-ce3a-43e6-8909-2b5cc08dbfee

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and 4bb43c1.

📒 Files selected for processing (8)
  • .gitignore
  • backend/campaigns/ai.py
  • backend/campaigns/migrations/0011 campaign_created.py
  • backend/campaigns/models.py
  • backend/campaigns/tasks.py
  • backend/campaigns/views.py
  • frontend/campaign-builder.html
  • frontend/dashboard.html

Comment thread backend/campaigns/ai.py Outdated
Comment thread backend/campaigns/tasks.py
Comment thread frontend/campaign-builder.html
Comment on lines +1887 to +1989
// ─── FLOWCHART VIEW (Mermaid) ───────
function escapeMermaidLabel(text) {
return String(text || '')
.replace(/"/g, '#quot;')
.replace(/\n/g, ' ')
.trim()
.slice(0, 60) || 'Untitled step';
}

function stepFlowchartLabel(step) {
const info = ICONS[step.type] || { label: step.type };
if (step.type === 'EMAIL') {
return step.subject ? `Email: ${step.subject}` : 'Email (no subject yet)';
}
if (step.type === 'WAIT') {
return `Wait ${step.delay_value || 1} ${step.delay_unit || 'day(s)'}`;
}
if (step.type === 'LINKEDIN') {
return `LinkedIn: ${(step.action || 'CONNECT').replace('_', ' ')}`;
}
if (step.type === 'SMS') {
return step.body ? `SMS: ${summarizeText(step.body, 30)}` : 'SMS message';
}
if (step.type === 'CALL') {
return 'Phone call';
}
if (step.type === 'MANUAL') {
return step.description ? summarizeText(step.description, 30) : 'Manual task';
}
return info.label || step.type;
}

function renderFlowchart() {
const container = document.getElementById('mermaidFlowchart');
if (!container || typeof mermaid === 'undefined') return;

if (!steps.length) {
container.removeAttribute('data-processed');
container.textContent = 'graph TD\n empty("No steps yet \u2014 add one in Canvas view")';
try { mermaid.init(undefined, container); } catch (e) { console.warn('Mermaid render failed', e); }
return;
}

const lines = ['graph TD', ' start((Start))'];

steps.forEach((step, i) => {
const nodeId = `step${i}`;
const isCondition = step.type && step.type.startsWith('CONDITION_');
const label = escapeMermaidLabel(stepFlowchartLabel(step));

lines.push(isCondition ? ` ${nodeId}{"${label}"}` : ` ${nodeId}["${label}"]`);

if (step.condition_branch && typeof step.condition_parent_index === 'number' && steps[step.condition_parent_index]) {
const parentId = `step${step.condition_parent_index}`;
const edgeLabel = step.condition_branch === 'yes' ? 'Yes' : 'No';
lines.push(` ${parentId} -->|${edgeLabel}| ${nodeId}`);
} else if (i === 0) {
lines.push(` start --> ${nodeId}`);
} else {
const prevIsCondition = steps[i - 1].type && steps[i - 1].type.startsWith('CONDITION_');
if (!prevIsCondition) {
lines.push(` step${i - 1} --> ${nodeId}`);
}
}

lines.push(` click ${nodeId} call editStepFromFlowchart("${i}")`);
});

container.removeAttribute('data-processed');
container.textContent = lines.join('\n');
try {
mermaid.init(undefined, container);
} catch (e) {
console.warn('Mermaid render failed', e);
}
}

window.editStepFromFlowchart = function (indexStr) {
const index = parseInt(indexStr, 10);
if (Number.isNaN(index) || !steps[index]) return;
selectedNodeIndex = index;
setCanvasView('canvas');
renderCanvas();
renderEditor();
};

function setCanvasView(view) {
canvasView = view;
const canvasBtn = document.getElementById('canvas-view-btn');
const flowchartBtn = document.getElementById('flowchart-view-btn');
const canvasEl = document.getElementById('canvas');
const flowchartEl = document.getElementById('flowchart-container');
if (canvasBtn) canvasBtn.classList.toggle('active', view === 'canvas');
if (flowchartBtn) flowchartBtn.classList.toggle('active', view === 'flowchart');
if (canvasEl) canvasEl.style.display = view === 'canvas' ? '' : 'none';
if (flowchartEl) flowchartEl.style.display = view === 'flowchart' ? 'block' : 'none';
if (view === 'flowchart') renderFlowchart();
}

// ─── RENDER CANVAS ──────────────────
function renderCanvas() {
window.campaignSteps = steps;
if (canvasView === 'flowchart') renderFlowchart();

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Mermaid initialization and flowchart rendering in the target file(s)
git ls-files | rg 'frontend/campaign-builder\.html$|frontend/.*campaign-builder.*\.html$'
rg -n "mermaid\.initialize|securityLevel|escapeMermaidLabel|click ${|editStepFromFlowchart|renderFlowchart|flowchart-container" frontend/campaign-builder.html

# Show the relevant sections with line numbers
sed -n '1840,2015p' frontend/campaign-builder.html

Repository: Kuldeeep18/LeadOrbit

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check Mermaid docs locally if vendored or referenced, and inspect any sanitization helpers
rg -n "DOMPurify|sanitize|securityLevel: 'strict'|securityLevel: 'loose'|antiscript|click .* call" frontend/campaign-builder.html frontend -g '!**/dist/**' -g '!**/build/**'

Repository: Kuldeeep18/LeadOrbit

Length of output: 678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe Mermaid directive/label semantics from installed docs if available in the repo
fd -HI "mermaid" .

Repository: Kuldeeep18/LeadOrbit

Length of output: 158


🌐 Web query:

Mermaid securityLevel strict click callbacks html labels loose antiscript documentation

💡 Result:

In Mermaid, the securityLevel configuration controls the trust level of parsed diagrams, specifically governing how HTML labels are handled and whether interactive features like click callbacks are permitted [1][2]. This setting was introduced as a security improvement to mitigate risks associated with rendering untrusted content [1][3]. The available security levels are: 1. strict (default): HTML tags in the text are encoded (rendered as plain text), and click functionality is disabled [1][4]. 2. antiscript: HTML tags in the text are allowed, except for script elements which are removed. Click functionality is enabled [1][5]. 3. loose: HTML tags are allowed, and click functionality is enabled [1][3]. 4. sandbox: Diagrams are rendered within a sandboxed iframe, which prevents JavaScript execution. This mode may hinder interactive features like scripts, popups, and links [1][6]. To configure the security level, you must use the mermaid.initialize method [1][3]: javascript mermaid.initialize({ securityLevel: 'loose' }); Note that when securityLevel is not set to loose, click event callbacks may be ignored [7]. Recent updates (as of late 2025) have introduced console warnings to alert developers when click callbacks are ignored due to restrictive security settings [7][8]. Users taking responsibility for the security of their diagram sources can explicitly set the securityLevel to allow desired interactivity [1][9].

Citations:


Stored XSS in the Mermaid flowchart escapeMermaidLabel() only strips quotes/newlines, so step text can still be treated as HTML while securityLevel: 'loose' keeps HTML labels and click callbacks enabled. Keep click-to-edit outside Mermaid or sanitize/encode the label text before rendering.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 1967-1967: React's useState should not be directly called
Context: setCanvasView('canvas')
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/campaign-builder.html` around lines 1887 - 1989, The Mermaid
flowchart currently interpolates unsanitized step text into labels while loose
security permits HTML execution. Update escapeMermaidLabel and the
renderFlowchart click handling to prevent user-controlled labels from becoming
HTML, preferably by disabling Mermaid click callbacks and using a safe encoded
label representation while preserving flowchart rendering and step editing
through a non-Mermaid interaction.

Comment thread frontend/dashboard.html Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/campaign-builder.html (1)

1889-1897: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Escape quotes to prevent Mermaid syntax injection (XSS).

While < and > are now correctly escaped to mitigate direct HTML injection, leaving quotes (" and ') unescaped introduces a Mermaid syntax injection vulnerability.

By not escaping quotes, an attacker can break out of the Mermaid label format (e.g., Node["..."]). With securityLevel: 'loose' still active, a user could input a payload like "] click A call alert(1) %% to inject arbitrary Mermaid directives, leading to XSS when the flowchart renders.

As flagged by the static analysis tools, hand-rolled sanitization is prone to bypasses. Please include quotes in your escaping chain.

🔒️ Proposed fix to securely escape quotes
             return String(text || '')
                 .replace(/&/g, '&amp;')
                 .replace(/</g, '&lt;')
                 .replace(/>/g, '&gt;')
+                .replace(/"/g, '&quot;')
+                .replace(/'/g, '&`#39`;')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/campaign-builder.html` around lines 1889 - 1897, Update the escaping
chain in the text-conversion function around `String(text || '')` to also encode
both double-quote and single-quote characters before returning the Mermaid label
text. Preserve the existing ampersand, angle-bracket, and replacement order so
user-controlled text cannot break Mermaid label syntax.

Source: Linters/SAST tools

🧹 Nitpick comments (1)
backend/campaigns/ai.py (1)

231-235: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Avoid potential lazy-loading of lead.organization.

Accessing lead.organization here will trigger a synchronous database query because it hasn't been pre-fetched upstream. While one extra query per background task is generally acceptable, consider adding lead__organization to the select_related clause where the lead is loaded to avoid unnecessary database hits. As per the provided relevant code snippets, lead__organization is missing from the select_related call in send_email_step in backend/campaigns/tasks.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/ai.py` around lines 231 - 235, Add lead__organization to
the select_related clause used when loading the lead in send_email_step,
ensuring the organization is prefetched before the AI personalization logic
accesses lead.organization in the relevant flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/campaigns/ai.py`:
- Line 263: The email-generation flow around genai.configure must stop mutating
the shared global Gemini configuration per request. Migrate this path to the
official google-genai client and instantiate a request-local client with
final_api_key, then use that client for subsequent model operations so
concurrent tenant requests cannot share credentials.

---

Duplicate comments:
In `@frontend/campaign-builder.html`:
- Around line 1889-1897: Update the escaping chain in the text-conversion
function around `String(text || '')` to also encode both double-quote and
single-quote characters before returning the Mermaid label text. Preserve the
existing ampersand, angle-bracket, and replacement order so user-controlled text
cannot break Mermaid label syntax.

---

Nitpick comments:
In `@backend/campaigns/ai.py`:
- Around line 231-235: Add lead__organization to the select_related clause used
when loading the lead in send_email_step, ensuring the organization is
prefetched before the AI personalization logic accesses lead.organization in the
relevant flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b8b886ae-62cb-4db0-a568-777996441b4e

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb43c1 and b40e1b3.

📒 Files selected for processing (5)
  • backend/campaigns/ai.py
  • backend/campaigns/migrations/0011_campaign_created.py
  • backend/campaigns/tasks.py
  • frontend/campaign-builder.html
  • frontend/dashboard.html
💤 Files with no reviewable changes (1)
  • frontend/dashboard.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/campaigns/tasks.py

Comment thread backend/campaigns/ai.py
final_api_key = active_key if active_key else api_key
from google.generativeai.types import RequestOptions

genai.configure(api_key=final_api_key)

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Race condition on global Gemini configuration.

genai.configure(api_key=final_api_key) mutates the global configuration for the Google Generative AI library. If your email workers use threads or coroutines (e.g., ThreadPoolExecutor, gevent) to process campaigns concurrently, tasks will overwrite each other's API keys. This can lead to authentication failures or cross-tenant billing issues where one tenant's request executes using another tenant's key.

Even if workers run sequentially (e.g., Celery prefork), re-configuring the global client on every email send is an anti-pattern that may cause unexpected connection reuse or resource leaks.

If you are using the older google-generativeai SDK, consider migrating to the official google-genai SDK where you can instantiate a localized, thread-safe client per request via client = genai.Client(api_key=final_api_key). Otherwise, ensure your worker concurrency model uses strictly isolated processes rather than threads to prevent key collisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/ai.py` at line 263, The email-generation flow around
genai.configure must stop mutating the shared global Gemini configuration per
request. Migrate this path to the official google-genai client and instantiate a
request-local client with final_api_key, then use that client for subsequent
model operations so concurrent tenant requests cannot share credentials.

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.

LO-060 [Advanced]: Automated Sandbox Mode for Lead Testing

1 participant