Skip to content

feat(agents): analytics + GEO tools for eve and Iris - #649

Open
mezotv wants to merge 23 commits into
feat/geofrom
feat/agent-analytics-tools
Open

feat(agents): analytics + GEO tools for eve and Iris#649
mezotv wants to merge 23 commits into
feat/geofrom
feat/agent-analytics-tools

Conversation

@mezotv

@mezotv mezotv commented Aug 2, 2026

Copy link
Copy Markdown
Member

Exposes everything in the stack below to the agents, so eve and Iris can read analytics and act on it.

  • New @notra/tools analytics tools: social analytics overview, engagement timeseries, top posts, posting performance, GEO overview, list A/B tests and create an A/B test.
  • Registers the seven tools in apps/agent.
  • Iris autonomy: new analytics and GEO capabilities with their parameter schemas, planner prompt updates, and a fix so a planner defect no longer surfaces as a 500.
  • Iris planner eval harness plus GEO judge-accuracy and consistency evals, with five recorded rounds.

Stack

  1. feat(analytics): X + LinkedIn analytics on ClickHouse #645 feat(analytics): X + LinkedIn analytics on ClickHouse
  2. feat(analytics): account leaderboard with tracked affiliates #646 feat(analytics): account leaderboard with tracked affiliates
  3. feat(experiments): social A/B tests #647 feat(experiments): social A/B tests
  4. feat(geo): AI visibility tracking #648 feat(geo): AI visibility tracking
  5. feat(agents): analytics + GEO tools for eve and Iris #649 feat(agents): analytics + GEO tools for eve and Iris 👈

Summary by cubic

Adds analytics and GEO tools so Eve and Iris can read social metrics, run A/B tests, act on GEO visibility, detect AI‑agent traffic, and classify prompt presence. Rebuilds dashboards into lightweight instrument panels with new donut/radar charts, model‑usage share, multi‑language GEO prompts, and faster cached queries.

  • New Features

    • Agents: tools in @notra/tools to read social overview, top posts, engagement timeseries, posting performance, GEO overview, and create/read A/B tests.
    • Analytics UI: Impressions Share donut, connect buttons, and a simplified leaderboard (removed track dialog and rank‑change; pinned window selector).
    • GEO UI: multi‑language prompt tracking with language performance and flags, Engine Radar, Share of Voice donut, prompt presence badges (training‑data vs. retrieval‑only vs. invisible), mention trend, and model usage market share (OpenRouter source) captured during scans.
    • AI traffic: @notra/beacon edge middleware with app ingest endpoint and proxy wiring; dashboard card with purpose donut plus request log.
    • UI primitives: new pie and radar chart components in @notra/ui, and instrument grid/module/reveal for the new panel layout.
    • Performance: materialized latest‑state views and Upstash Redis caching for Tinybird queries in @notra/analytics to speed charts.
  • Bug Fixes

    • Charts: tooltips show inline headings and hovered‑day labels in analytics and GEO.
    • Leaderboard: render verification badges and window label; square business avatars; reliable connect buttons.
    • GEO: contained request log table and cleaned table body corners; rebalanced overview grid and split AI traffic into donut + detailed log.
    • Analytics: purge removed accounts from ClickHouse/Tinybird and add pointer cursors to interactive controls.

Written for commit 50839b4. Summary will update on new commits.

Review in cubic

Summary by Comp AI

No blocking issues found.

Written for commit 50839b4. New commits will trigger a re-review. Generated by Comp AI.

@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
notra Error Error Aug 3, 2026 12:29pm
4 Skipped Deployments
Project Deployment Actions Updated (UTC)
notra-agent Skipped Skipped Aug 3, 2026 12:29pm
notra-console Skipped Skipped Aug 3, 2026 12:29pm
notra-onboarding-agent Skipped Skipped Aug 3, 2026 12:29pm
notra-web Skipped Skipped Aug 3, 2026 12:29pm

Request Review

@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@comp-ai-code-review

comp-ai-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

Comp AI code review complete — no issues found.

Commit 50839b4 · Posted by Comp AI Code Reviews.

@capy-ai

capy-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

React Doctor found 30 new issues in 17 files · 3 errors & 27 warnings · score 70 / 100 (Needs work) · 17 fixed · vs feat/geo

Errors

27 warnings

evals/iris-planner/run.ts

  • ⚠️ L263 Chained array iterations js-combine-iterations
  • ⚠️ L283 await inside a loop async-await-in-loop

src/app/(dashboard)/[slug]/analytics/page-client.tsx

  • ⚠️ L73 Large component is hard to read and change no-giant-component
  • ⚠️ L102 React Compiler can't optimize this set-state-in-effect

src/app/(dashboard)/[slug]/geo/page-client.tsx

  • ⚠️ L99 React Compiler can't optimize this set-state-in-effect

src/autonomy/capabilities.ts

  • ⚠️ L844 Spread copy before sort() js-tosorted-immutable

src/components/analytics/impressions-share-card.tsx

  • ⚠️ L34 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L35 Chained array iterations js-combine-iterations

src/components/analytics/summary-stats.tsx

  • ⚠️ L13 Manual memoization in compiler-managed code react-compiler-no-manual-memoization

src/components/geo/ai-traffic-card.tsx

  • ⚠️ L88 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L126 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L128 Missing effect dependencies exhaustive-deps
  • ⚠️ L130 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L132 Missing effect dependencies exhaustive-deps

src/components/geo/engine-radar-card.tsx

  • ⚠️ L48 Manual memoization in compiler-managed code react-compiler-no-manual-memoization

src/components/geo/geo-summary-stats.tsx

  • ⚠️ L19 Manual memoization in compiler-managed code react-compiler-no-manual-memoization

src/components/geo/mention-trend-card.tsx

  • ⚠️ L24 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L29 Manual memoization in compiler-managed code react-compiler-no-manual-memoization

src/components/geo/model-usage-card.tsx

  • ⚠️ L68 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L70 Missing effect dependencies exhaustive-deps
  • ⚠️ L72 Manual memoization in compiler-managed code react-compiler-no-manual-memoization
  • ⚠️ L74 Missing effect dependencies exhaustive-deps

src/components/geo/prompt-results-preview.tsx

  • ⚠️ L71 Manual memoization in compiler-managed code react-compiler-no-manual-memoization

src/components/geo/share-of-voice-donut.tsx

  • ⚠️ L32 Manual memoization in compiler-managed code react-compiler-no-manual-memoization

src/components/instrument/instrument-reveal.tsx

  • ⚠️ L4 Full Framer Motion import use-lazy-motion

src/lib/orpc/routers/analytics.ts

  • ⚠️ L223 Chained array iterations js-combine-iterations

src/utils/geo-charts.ts

  • ⚠️ L86 Spread copy before sort() js-tosorted-immutable

Reviewed by React Doctor for commit d9751ba. See inline comments for fixes.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds seven analytics/GEO tools to Eve and extends Iris with analytics-aware planning and A/B-test capabilities, together with planner and GEO evaluation harnesses.

  • Adds social overview, timeseries, top-post, posting-performance, GEO, and experiment tools.
  • Adds Iris analytics and experiment capability schemas, execution, labels, and planner guidance.
  • Adds recorded planner evaluations and GEO judge accuracy/consistency scripts.

Confidence Score: 4/5

The A/B-test workflow needs to expose platform post IDs before merging, otherwise Eve and Iris cannot create experiments grounded in the analytics results they are instructed to use.

Both agent paths discard platform_post_id at the analytics boundary while requiring that identifier for experiment creation, leaving the newly advertised workflow unable to pass valid variant IDs.

Files Needing Attention: packages/tools/src/analytics/get-top-posts.ts, packages/ai/src/autonomy/capabilities.ts, packages/ai/src/prompts/iris-planner.ts

Important Files Changed

Filename Overview
packages/tools/src/analytics/get-top-posts.ts Adds the top-posts tool but drops the platform post identifier required by the new A/B-test creation tool.
packages/tools/src/analytics/create-ab-test.ts Adds model-facing experiment creation using required platform IDs, but its documented source does not expose those IDs.
packages/ai/src/autonomy/capabilities.ts Adds Iris analytics and experiment execution; the analytics external reference similarly omits IDs needed by experiment tasks.
packages/ai/src/prompts/iris-planner.ts Adds data-first planning instructions, including an experiment workflow that cannot obtain the required IDs from its stated dependency.
packages/tools/src/analytics/get-ab-tests.ts Adds tenant-scoped experiment listing and live metric lookup without an independently identified defect.
packages/ai/src/autonomy/planner.ts Narrows the planner model result to output and usage, preventing unrelated result fields from reaching downstream parsing.

Sequence Diagram

sequenceDiagram
  participant Agent as Eve / Iris
  participant Analytics as Analytics read
  participant Tinybird
  participant Experiment as Experiment create
  participant DB as socialExperiments
  Agent->>Analytics: Request top posts
  Analytics->>Tinybird: queryTopPosts(organization_id)
  Tinybird-->>Analytics: Posts including platform_post_id
  Analytics-->>Agent: Posts without platform_post_id
  Agent->>Experiment: Create test requiring two post IDs
  Experiment->>DB: Persist supplied IDs
  Note over Agent,DB: Valid IDs cannot be sourced from the advertised read
Loading

Reviews (1): Last reviewed commit: "feat(agents): add analytics and GEO tool..." | Re-trigger Greptile

Comment on lines +38 to +47
content: row.content,
url: row.url,
posted_at: row.posted_at,
likes: row.likes,
replies: row.replies,
reposts: row.reposts,
impressions: row.impressions,
engagement: row.engagement,
})),
};

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.

P1 Analytics drops required post IDs

When Eve or Iris creates an A/B test from the new analytics reads, both result mappings discard the available platform_post_id even though experiment creation requires those IDs, forcing the agents to omit the experiment or submit invented IDs that persist with empty or zero-valued metrics.

Knowledge Base Used:

@vercel
vercel Bot temporarily deployed to Preview – notra-agent August 2, 2026 11:22 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-web August 2, 2026 11:22 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-onboarding-agent August 2, 2026 11:22 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-console August 2, 2026 11:22 Inactive

@comp-ai-code-review comp-ai-code-review 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.

No blocking issues found across the changed files.

1 other commit review still in progress for this PR — findings may follow.

Commit 9514ad2 · Posted by Comp AI Code Reviews.

@comp-ai-code-review comp-ai-code-review 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.

8 issues found across 8 files.

Prompt for AI agents (all issues)
Check whether each issue below is valid; if so, find the root cause and fix it. Read the referenced code to confirm the problem before changing it, and use sub-agents to handle independent issues in parallel.

<file name="apps/agent/agent/tools/get_social_analytics_overview.ts">
<issue n="1" at="apps/agent/agent/tools/get_social_analytics_overview.ts:46-50" severity="MEDIUM">Raw backend error messages surfaced to tool caller via catch block — This target file is a thin wrapper (`export default createGetSocialAnalyticsOverviewTool()`) that delegates to the implementation in `packages/tools/src/analytics/get-social-analytics-overview.ts`. That implementation's `execute` catch block (lines 46-50) returns `${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}` directly as the tool result. In an AI-agent context, tool return values are fed to the LLM and frequently relayed verbatim to end users. Because `querySocialOverview` calls the Tinybird SDK, a failing request can propagate third-party backend error text — e.g. Tinybird endpoint/pipe names, SQL fragments, rate-limit headers, or 'Unauthorized / invalid token' hints that reveal the analytics backend configuration and confirm whether `TINYBIRD_TOKEN` is set. The query is scoped to the caller's own `organization_id`, so cross-organization data is not leaked, but internal backend/integration details are. Positive note: the tool otherwise correctly sources `organizationId` from `ctx.session.auth` (not user tool input, since the input schema is an empty object), uses Tinybird's parameterized `{{String(organization_id)}}` template (no SQL injection), and validates all inputs — so this error-handling hygiene issue is the only finding. Fix: In production, return only the generic `ANALYTICS_QUERY_FAILED_MESSAGE` to the tool caller and log the full `error` (including stack) server-side via the agent's structured logger. If diagnostics are needed, gate detailed error text behind a non-production environment flag so internal Tinybird backend details are never surfaced to end users through the agent.</issue>
</file>

<file name="apps/agent/agent/tools/get_top_posts.ts">
<issue n="2" at="apps/agent/agent/tools/get_top_posts.ts:48-52" severity="MEDIUM">Raw backend error messages surfaced to tool caller via catch block — This target file is a thin wrapper (`export default createGetTopPostsTool()`) that delegates to the implementation in `packages/tools/src/analytics/get-top-posts.ts`. That implementation's `execute` catch block (lines 48-52) returns `${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}` directly as the tool result. In an AI-agent context, tool return values are fed to the LLM and often relayed verbatim to end users. Since `queryTopPosts` invokes the Tinybird SDK, a failing request can expose third-party backend error text — Tinybird pipe/endpoint names, SQL fragments, rate-limit info, or token-validity hints that disclose the analytics backend configuration. The query is scoped to the caller's own `organization_id` (no cross-org leak), and `limit` is zod-validated to int 1-25 with default 10 and passed to Tinybird's parameterized `{{Int32(limit, 10)}}` template (no SQL injection). `organizationId` is correctly derived from `ctx.session.auth`, not user tool input. The raw-error disclosure is the only finding. Fix: In production, return only the generic `ANALYTICS_QUERY_FAILED_MESSAGE` to the tool caller and log the full `error` (including stack) server-side via the agent's structured logger. Gate any detailed error text behind a non-production environment flag so internal Tinybird backend details are never surfaced to end users through the agent.</issue>
</file>

<file name="packages/tools/src/analytics/get-social-analytics-overview.ts">
<issue n="3" at="packages/tools/src/analytics/get-social-analytics-overview.ts:31-34" severity="MEDIUM">Raw backend error messages returned to the agent context — The catch block (L31-34) appends the raw exception text to ANALYTICS_QUERY_FAILED_MESSAGE and returns it: `${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}`. When the underlying Tinybird client throws, the error message can include internal backend details (request URLs, datasource names, partial SQL context, token presence indicators, or stack traces from the SDK). This string is returned from the tool's execute() into the agent/LLM context and may be surfaced back to the end user via the agent's reply. The scanner's 'insecure-crypto / weak cipher algorithm' flag at L15 is a false positive — there is no cryptographic code in this file; line 15 is the `inputSchema` assignment. The Tinybird queries themselves are safe: the endpoint definitions in packages/analytics/src/tinybird/endpoints.ts use parameterized Tinybird templates (`{{String(organization_id)}}`) rather than string interpolation, so there is no SQL injection. The organizationId is sourced from the trusted session context (ctx.session.auth.current?.attributes.organizationId), which is populated by the authenticated caller (Vercel OIDC project verification or HTTP basic service account in apps/agent/agent/channels/eve.ts) rather than directly from end-user input. Fix: Return a generic, fixed error message to the agent context and log the detailed error server-side only. Do not interpolate `error.message` / `String(error)` into tool output, since the agent may relay it to end users.</issue>
</file>

<file name="packages/tools/src/analytics/get-top-posts.ts">
<issue n="4" at="packages/tools/src/analytics/get-top-posts.ts:36-39" severity="MEDIUM">Raw backend error messages returned to the agent context — The catch block (L36-39) appends the raw exception text to ANALYTICS_QUERY_FAILED_MESSAGE and returns it: `${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}`. When the underlying Tinybird client throws, the error message can include internal backend details (request URLs, datasource names, partial SQL context, token presence indicators, or stack traces from the SDK). This string is returned from the tool's execute() into the agent/LLM context and may be surfaced back to the end user via the agent's reply. The scanner's 'insecure-crypto / weak cipher algorithm' flag at L15 is a false positive — there is no cryptographic code in this file; line 15 is `return defineTool({`. The Tinybird queries themselves are safe: the endpoint definitions in packages/analytics/src/tinybird/endpoints.ts use parameterized Tinybird templates (`{{String(organization_id)}}`, `{{Int32(limit, 10)}}`) rather than string interpolation, so there is no SQL injection, and `limit` is additionally constrained by the Zod schema (z.number().int().min(1).max(25).default(10)). The organizationId is sourced from the trusted session context (ctx.session.auth.current?.attributes.organizationId), which is populated by the authenticated caller (Vercel OIDC project verification or HTTP basic service account in apps/agent/agent/channels/eve.ts) rather than directly from end-user input. Fix: Return a generic, fixed error message to the agent context and log the detailed error server-side only. Do not interpolate `error.message` / `String(error)` into tool output, since the agent may relay it to end users.</issue>
</file>

<file name="apps/agent/agent/tools/create_ab_test.ts">
<issue n="5" at="apps/agent/agent/tools/create_ab_test.ts:22-33" severity="MEDIUM">create_ab_test accepts arbitrary post IDs without ownership validation or length bounds — The create_ab_test tool (implementation in packages/tools/src/analytics/create-ab-test.ts, re-exported by this file) inserts an A/B experiment row using `variantAPostId` and `variantBPostId` taken directly from LLM/user input. The input schema (`createAbTestInputSchema`) only constrains these as `z.string().min(1)` with no maximum length and no validation that the post IDs belong to the calling organization. The only check is that A != B. The row is then written to the `social_experiments` table scoped to the session's `organizationId` (which IS trustworthy — derived from the authenticated dashboard/API caller, not user input). Trust model analysis: the `organizationId` comes from `requireOrganizationId(ctx)`, which reads `ctx.session.auth...attributes.organizationId`, set from the `x-notra-organization-id` header by trusted, authenticated callers (dashboard/API derive it from authenticated API keys/sessions, not user input), so this is NOT a cross-tenant access bypass. However, because post IDs are never checked against the org's actual posts (e.g. via Tinybird `post_metrics_lookup` or the social_posts table), a caller can create experiment rows referencing arbitrary/garbage or extremely long strings. Impact is limited to data pollution within the caller's own organization: `get_ab_tests` resolves metrics through Tinybird's `post_metrics_lookup` pipe which is itself scoped by `organization_id`, so a cross-organization post ID simply yields null metrics rather than leaking another tenant's data. The DB column is `text`, so no crash, but there is no length cap to constrain storage abuse. This is a data-integrity/validation weakness rather than an exploitable security vulnerability. Fix: Validate that variantAPostId and variantBPostId correspond to real posts owned by the calling organization before inserting (e.g. run them through Tinybird's post_metrics_lookup scoped by organization_id, or query social_posts WHERE organization_id = ? AND platform_post_id IN (...), and reject if any are missing). Also add a reasonable max length (e.g. .max(100)) to the post ID strings to prevent storage abuse, consistent with platform post id formats.</issue>
</file>

<file name="packages/ai/src/prompts/iris-planner.ts">
<issue n="6" at="packages/ai/src/prompts/iris-planner.ts:206-211" severity="MEDIUM">recentActionSummaries interpolated into planner prompt without sanitizeUntrustedText (inconsistent with signalSummaries) — In buildIrisPlannerUserPrompt, signalSummaries are passed through sanitizeUntrustedText and placed inside the SIGNAL_DELIMITER_OPEN/CLOSE block (L211), and mandate.objective is also sanitized (L188). However, recentActionSummaries are interpolated raw into the <recent-actions> block via describeList(input.recentActionSummaries, ...) (L206) with neither sanitizeUntrustedText nor delimiter wrapping. The system prompt only instructs the model to distrust text inside the signal delimiters, so content in <recent-actions> is implicitly treated as trusted. I traced the data flow: loadRecentActionSummaries (apps/dashboard/src/lib/iris/history.ts) builds each summary from a fixed template `${capabilityName} ${status} at ${createdAt.toISOString()}` using capabilityName (validated against the capability catalog enum), status (an enum), and an ISO date. Because these are constrained enum-style DB columns rather than attacker-controlled free text, this is NOT currently exploitable — hence BUG rather than a security severity. The concern is latent: if loadRecentActionSummaries is later extended to include free-text fields (e.g., an action's goal title or reason, which the planner derives from untrusted signal/commit data), the unsanitized interpolation would silently become an indirect prompt-injection path that bypasses the delimiter-trust boundary the rest of the file enforces. The scanner's 'insecure-crypto' flags on this file are false positives — there is no cryptographic code anywhere in the file (confirmed by grep for crypto/hash/AES/jwt/Math.random). Fix: Apply sanitizeUntrustedText consistently: change L206 to describeList(input.recentActionSummaries.map(sanitizeUntrustedText), "- none recorded yet") to match the treatment of signalSummaries, so the trust boundary holds even if the summary format is later changed to include free text.</issue>
</file>

<file name="packages/tools/src/analytics/create-ab-test.ts">
<issue n="7" at="packages/tools/src/analytics/create-ab-test.ts:11-39" severity="MEDIUM">A/B test variant post IDs are not validated for ownership/existence before insert — createCreateAbTestTool accepts `variantAPostId` and `variantBPostId` as free-form `z.string().min(1)` values (see schemas/analytics-tools.ts) and inserts them directly into the `social_experiments` table without verifying that those post IDs belong to the caller's organization or even exist. The only check is that the two IDs differ. The experiment row itself is correctly scoped to the caller's `organizationId` (from the authenticated session via requireOrganizationId), and when metrics are later looked up in get-ab-tests.ts the Tinybird `post_metrics_lookup` pipe is called with `organization_id: organizationId` plus the post_ids, so a post ID belonging to another org would return no metrics (no cross-tenant data exfiltration). The practical impact is therefore limited to data-quality/logic bugs: a user (or a confused agent) can create experiments referencing arbitrary or non-existent post IDs, producing A/B tests that silently never resolve. This is not a security boundary violation because no other tenant's data is ever returned, but it is a missing ownership check worth noting. Note: the scanner's 'insecure-crypto' / 'weak cipher algorithm' flags on the import line and 'crypto-usage' on `crypto.randomUUID()` are false positives — `crypto.randomUUID()` is the CSPRNG-backed Web Crypto API and is the correct way to generate record IDs; no weak cipher is used anywhere in this file. Fix: Before inserting, validate that variantAPostId/variantBPostId correspond to real posts owned by the caller's organization (e.g., query the org-scoped posts/social_posts table or Tinybird datasource and reject unknown IDs). Optionally add a DB-level foreign key to an org-scoped posts table if such a table exists.</issue>
</file>

<file name="packages/tools/src/schemas/analytics-tools.ts">
<issue n="8" at="packages/tools/src/schemas/analytics-tools.ts:52-57" severity="MEDIUM">createAbTestInputSchema accepts variant post IDs without ownership validation — The createAbTestInputSchema defines variantAPostId and variantBPostId as free-form strings (z.string().min(1)) described as 'Platform post id of variant A/B (from get_top_posts)'. In the consuming tool (packages/tools/src/analytics/create-ab-test.ts), these user/LLM-supplied post IDs are inserted directly into the socialExperiments table without any verification that the referenced posts belong to the calling organization. The socialExperiments table schema (packages/db/src/schema.ts L1501-1510) stores variantAPostId/variantBPostId as plain text columns with no foreign key or organization-scoped constraint. While the actual security impact is limited — the read path in get-ab-tests.ts queries Tinybird scoped by organization_id, so metrics for a foreign post ID would return no data rather than leaking another tenant's metrics — this still represents a data integrity gap. A user could create experiments referencing arbitrary or garbage post IDs, polluting the experiments table. The scanner's 'weak cipher algorithm' hits on lines 12, 22, 32, 42, 52, 57, 61, 65, 69, 73 are all false positives: this file contains only Zod schema definitions with .default() values and enums, and no cryptographic code whatsoever. The actual encryption implementation in packages/db/src/utils/integration-encryption.ts uses proper AES-256-GCM with random IVs and auth tags. Fix: Before inserting a new A/B test, validate that variantAPostId and variantBPostId correspond to posts that belong to the calling organization. Query Tinybird's top posts endpoint (or the socialPosts data source) scoped by organization_id and verify both post IDs exist in the result set before allowing the insert. This prevents users from creating experiments referencing arbitrary post IDs.</issue>
</file>

Commit d32a58e · Posted by Comp AI Code Reviews.

Comment on lines +31 to +34
return ANALYTICS_NOT_CONFIGURED_MESSAGE;
}

return {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: Raw backend error messages returned to the agent context

The catch block (L31-34) appends the raw exception text to ANALYTICS_QUERY_FAILED_MESSAGE and returns it: ${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}. When the underlying Tinybird client throws, the error message can include internal backend details (request URLs, datasource names, partial SQL context, token presence indicators, or stack traces from the SDK). This string is returned from the tool's execute() into the agent/LLM context and may be surfaced back to the end user via the agent's reply. The scanner's 'insecure-crypto / weak cipher algorithm' flag at L15 is a false positive — there is no cryptographic code in this file; line 15 is the inputSchema assignment. The Tinybird queries themselves are safe: the endpoint definitions in packages/analytics/src/tinybird/endpoints.ts use parameterized Tinybird templates ({{String(organization_id)}}) rather than string interpolation, so there is no SQL injection. The organizationId is sourced from the trusted session context (ctx.session.auth.current?.attributes.organizationId), which is populated by the authenticated caller (Vercel OIDC project verification or HTTP basic service account in apps/agent/agent/channels/eve.ts) rather than directly from end-user input.

Suggestion: Return a generic, fixed error message to the agent context and log the detailed error server-side only. Do not interpolate error.message / String(error) into tool output, since the agent may relay it to end users.

Prompt for AI agents
Check whether this issue is valid; if so, find the root cause and fix it. Read the referenced code to confirm the problem before changing it.

<issue at="packages/tools/src/analytics/get-social-analytics-overview.ts:31-34" severity="MEDIUM">Raw backend error messages returned to the agent context — The catch block (L31-34) appends the raw exception text to ANALYTICS_QUERY_FAILED_MESSAGE and returns it: `${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}`. When the underlying Tinybird client throws, the error message can include internal backend details (request URLs, datasource names, partial SQL context, token presence indicators, or stack traces from the SDK). This string is returned from the tool's execute() into the agent/LLM context and may be surfaced back to the end user via the agent's reply. The scanner's 'insecure-crypto / weak cipher algorithm' flag at L15 is a false positive — there is no cryptographic code in this file; line 15 is the `inputSchema` assignment. The Tinybird queries themselves are safe: the endpoint definitions in packages/analytics/src/tinybird/endpoints.ts use parameterized Tinybird templates (`{{String(organization_id)}}`) rather than string interpolation, so there is no SQL injection. The organizationId is sourced from the trusted session context (ctx.session.auth.current?.attributes.organizationId), which is populated by the authenticated caller (Vercel OIDC project verification or HTTP basic service account in apps/agent/agent/channels/eve.ts) rather than directly from end-user input. Fix: Return a generic, fixed error message to the agent context and log the detailed error server-side only. Do not interpolate `error.message` / `String(error)` into tool output, since the agent may relay it to end users.</issue>

Commit d32a58e.

Comment on lines +36 to +39
posts: result.data.map((row) => ({
provider: row.provider,
content: row.content,
url: row.url,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: Raw backend error messages returned to the agent context

The catch block (L36-39) appends the raw exception text to ANALYTICS_QUERY_FAILED_MESSAGE and returns it: ${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}. When the underlying Tinybird client throws, the error message can include internal backend details (request URLs, datasource names, partial SQL context, token presence indicators, or stack traces from the SDK). This string is returned from the tool's execute() into the agent/LLM context and may be surfaced back to the end user via the agent's reply. The scanner's 'insecure-crypto / weak cipher algorithm' flag at L15 is a false positive — there is no cryptographic code in this file; line 15 is return defineTool({. The Tinybird queries themselves are safe: the endpoint definitions in packages/analytics/src/tinybird/endpoints.ts use parameterized Tinybird templates ({{String(organization_id)}}, {{Int32(limit, 10)}}) rather than string interpolation, so there is no SQL injection, and limit is additionally constrained by the Zod schema (z.number().int().min(1).max(25).default(10)). The organizationId is sourced from the trusted session context (ctx.session.auth.current?.attributes.organizationId), which is populated by the authenticated caller (Vercel OIDC project verification or HTTP basic service account in apps/agent/agent/channels/eve.ts) rather than directly from end-user input.

Suggestion: Return a generic, fixed error message to the agent context and log the detailed error server-side only. Do not interpolate error.message / String(error) into tool output, since the agent may relay it to end users.

Prompt for AI agents
Check whether this issue is valid; if so, find the root cause and fix it. Read the referenced code to confirm the problem before changing it.

<issue at="packages/tools/src/analytics/get-top-posts.ts:36-39" severity="MEDIUM">Raw backend error messages returned to the agent context — The catch block (L36-39) appends the raw exception text to ANALYTICS_QUERY_FAILED_MESSAGE and returns it: `${ANALYTICS_QUERY_FAILED_MESSAGE} ${error instanceof Error ? error.message : String(error)}`. When the underlying Tinybird client throws, the error message can include internal backend details (request URLs, datasource names, partial SQL context, token presence indicators, or stack traces from the SDK). This string is returned from the tool's execute() into the agent/LLM context and may be surfaced back to the end user via the agent's reply. The scanner's 'insecure-crypto / weak cipher algorithm' flag at L15 is a false positive — there is no cryptographic code in this file; line 15 is `return defineTool({`. The Tinybird queries themselves are safe: the endpoint definitions in packages/analytics/src/tinybird/endpoints.ts use parameterized Tinybird templates (`{{String(organization_id)}}`, `{{Int32(limit, 10)}}`) rather than string interpolation, so there is no SQL injection, and `limit` is additionally constrained by the Zod schema (z.number().int().min(1).max(25).default(10)). The organizationId is sourced from the trusted session context (ctx.session.auth.current?.attributes.organizationId), which is populated by the authenticated caller (Vercel OIDC project verification or HTTP basic service account in apps/agent/agent/channels/eve.ts) rather than directly from end-user input. Fix: Return a generic, fixed error message to the agent context and log the detailed error server-side only. Do not interpolate `error.message` / `String(error)` into tool output, since the agent may relay it to end users.</issue>

Commit d32a58e.

Comment on lines +11 to +39
"Start a social A/B test comparing two published posts on one metric. Use post ids from get_top_posts (the platform_post_id field). The test tracks both posts' live metrics until a winner is declared in the dashboard. Use this to run data-driven experiments on hooks, formats, or topics.",
inputSchema: createAbTestInputSchema,
async execute(input, ctx) {
const organizationId = requireOrganizationId(ctx);

if (input.variantAPostId === input.variantBPostId) {
return "Variant A and variant B must be different posts.";
}

try {
const [created] = await db
.insert(socialExperiments)
.values({
id: crypto.randomUUID(),
organizationId,
name: input.name,
hypothesis: input.hypothesis ?? null,
provider: input.provider,
variantAPostId: input.variantAPostId,
variantBPostId: input.variantBPostId,
metric: input.metric,
})
.returning({ id: socialExperiments.id });

return {
experiment_id: created?.id ?? null,
status: "running",
note: "Metrics update on every analytics sync. Check results with get_ab_tests.",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: A/B test variant post IDs are not validated for ownership/existence before insert

createCreateAbTestTool accepts variantAPostId and variantBPostId as free-form z.string().min(1) values (see schemas/analytics-tools.ts) and inserts them directly into the social_experiments table without verifying that those post IDs belong to the caller's organization or even exist. The only check is that the two IDs differ. The experiment row itself is correctly scoped to the caller's organizationId (from the authenticated session via requireOrganizationId), and when metrics are later looked up in get-ab-tests.ts the Tinybird post_metrics_lookup pipe is called with organization_id: organizationId plus the post_ids, so a post ID belonging to another org would return no metrics (no cross-tenant data exfiltration). The practical impact is therefore limited to data-quality/logic bugs: a user (or a confused agent) can create experiments referencing arbitrary or non-existent post IDs, producing A/B tests that silently never resolve. This is not a security boundary violation because no other tenant's data is ever returned, but it is a missing ownership check worth noting. Note: the scanner's 'insecure-crypto' / 'weak cipher algorithm' flags on the import line and 'crypto-usage' on crypto.randomUUID() are false positives — crypto.randomUUID() is the CSPRNG-backed Web Crypto API and is the correct way to generate record IDs; no weak cipher is used anywhere in this file.

Suggestion: Before inserting, validate that variantAPostId/variantBPostId correspond to real posts owned by the caller's organization (e.g., query the org-scoped posts/social_posts table or Tinybird datasource and reject unknown IDs). Optionally add a DB-level foreign key to an org-scoped posts table if such a table exists.

Prompt for AI agents
Check whether this issue is valid; if so, find the root cause and fix it. Read the referenced code to confirm the problem before changing it.

<issue at="packages/tools/src/analytics/create-ab-test.ts:11-39" severity="MEDIUM">A/B test variant post IDs are not validated for ownership/existence before insert — createCreateAbTestTool accepts `variantAPostId` and `variantBPostId` as free-form `z.string().min(1)` values (see schemas/analytics-tools.ts) and inserts them directly into the `social_experiments` table without verifying that those post IDs belong to the caller's organization or even exist. The only check is that the two IDs differ. The experiment row itself is correctly scoped to the caller's `organizationId` (from the authenticated session via requireOrganizationId), and when metrics are later looked up in get-ab-tests.ts the Tinybird `post_metrics_lookup` pipe is called with `organization_id: organizationId` plus the post_ids, so a post ID belonging to another org would return no metrics (no cross-tenant data exfiltration). The practical impact is therefore limited to data-quality/logic bugs: a user (or a confused agent) can create experiments referencing arbitrary or non-existent post IDs, producing A/B tests that silently never resolve. This is not a security boundary violation because no other tenant's data is ever returned, but it is a missing ownership check worth noting. Note: the scanner's 'insecure-crypto' / 'weak cipher algorithm' flags on the import line and 'crypto-usage' on `crypto.randomUUID()` are false positives — `crypto.randomUUID()` is the CSPRNG-backed Web Crypto API and is the correct way to generate record IDs; no weak cipher is used anywhere in this file. Fix: Before inserting, validate that variantAPostId/variantBPostId correspond to real posts owned by the caller's organization (e.g., query the org-scoped posts/social_posts table or Tinybird datasource and reject unknown IDs). Optionally add a DB-level foreign key to an org-scoped posts table if such a table exists.</issue>

Commit d32a58e.

Comment on lines +52 to +57
.describe("Short descriptive name for the experiment."),
hypothesis: z
.string()
.max(500)
.optional()
.describe("What you expect to learn and why."),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIUM: createAbTestInputSchema accepts variant post IDs without ownership validation

The createAbTestInputSchema defines variantAPostId and variantBPostId as free-form strings (z.string().min(1)) described as 'Platform post id of variant A/B (from get_top_posts)'. In the consuming tool (packages/tools/src/analytics/create-ab-test.ts), these user/LLM-supplied post IDs are inserted directly into the socialExperiments table without any verification that the referenced posts belong to the calling organization. The socialExperiments table schema (packages/db/src/schema.ts L1501-1510) stores variantAPostId/variantBPostId as plain text columns with no foreign key or organization-scoped constraint. While the actual security impact is limited — the read path in get-ab-tests.ts queries Tinybird scoped by organization_id, so metrics for a foreign post ID would return no data rather than leaking another tenant's metrics — this still represents a data integrity gap. A user could create experiments referencing arbitrary or garbage post IDs, polluting the experiments table. The scanner's 'weak cipher algorithm' hits on lines 12, 22, 32, 42, 52, 57, 61, 65, 69, 73 are all false positives: this file contains only Zod schema definitions with .default() values and enums, and no cryptographic code whatsoever. The actual encryption implementation in packages/db/src/utils/integration-encryption.ts uses proper AES-256-GCM with random IVs and auth tags.

Suggestion: Before inserting a new A/B test, validate that variantAPostId and variantBPostId correspond to posts that belong to the calling organization. Query Tinybird's top posts endpoint (or the socialPosts data source) scoped by organization_id and verify both post IDs exist in the result set before allowing the insert. This prevents users from creating experiments referencing arbitrary post IDs.

Prompt for AI agents
Check whether this issue is valid; if so, find the root cause and fix it. Read the referenced code to confirm the problem before changing it.

<issue at="packages/tools/src/schemas/analytics-tools.ts:52-57" severity="MEDIUM">createAbTestInputSchema accepts variant post IDs without ownership validation — The createAbTestInputSchema defines variantAPostId and variantBPostId as free-form strings (z.string().min(1)) described as 'Platform post id of variant A/B (from get_top_posts)'. In the consuming tool (packages/tools/src/analytics/create-ab-test.ts), these user/LLM-supplied post IDs are inserted directly into the socialExperiments table without any verification that the referenced posts belong to the calling organization. The socialExperiments table schema (packages/db/src/schema.ts L1501-1510) stores variantAPostId/variantBPostId as plain text columns with no foreign key or organization-scoped constraint. While the actual security impact is limited — the read path in get-ab-tests.ts queries Tinybird scoped by organization_id, so metrics for a foreign post ID would return no data rather than leaking another tenant's metrics — this still represents a data integrity gap. A user could create experiments referencing arbitrary or garbage post IDs, polluting the experiments table. The scanner's 'weak cipher algorithm' hits on lines 12, 22, 32, 42, 52, 57, 61, 65, 69, 73 are all false positives: this file contains only Zod schema definitions with .default() values and enums, and no cryptographic code whatsoever. The actual encryption implementation in packages/db/src/utils/integration-encryption.ts uses proper AES-256-GCM with random IVs and auth tags. Fix: Before inserting a new A/B test, validate that variantAPostId and variantBPostId correspond to posts that belong to the calling organization. Query Tinybird's top posts endpoint (or the socialPosts data source) scoped by organization_id and verify both post IDs exist in the result set before allowing the insert. This prevents users from creating experiments referencing arbitrary post IDs.</issue>

Commit d32a58e.

@vercel
vercel Bot temporarily deployed to Preview – notra-onboarding-agent August 2, 2026 11:29 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-web August 2, 2026 11:29 Inactive

@comp-ai-code-review comp-ai-code-review 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.

No blocking issues found across the changed files.

Commit 14d90d9 · Posted by Comp AI Code Reviews.

@vercel
vercel Bot temporarily deployed to Preview – notra-onboarding-agent August 2, 2026 18:07 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-agent August 2, 2026 18:07 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-console August 2, 2026 18:07 Inactive
@vercel
vercel Bot temporarily deployed to Preview – notra-web August 2, 2026 18:07 Inactive

@comp-ai-code-review comp-ai-code-review 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.

No blocking issues found across the changed files.

Commit a015938 · Posted by Comp AI Code Reviews.

const [stage, setStage] = useState(0);
const ready = !isOverviewPending;

useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/effect-needs-cleanup (error)

setTimeout creates a timer in useEffect without guaranteed cleanup. Return a cleanup function that owns every allocation so it does not leak after unmount.

Fix → Return a cleanup function that stops the subscription or timer: return () => target.removeEventListener(name, handler) for listeners, return () => clearInterval(id) or clearTimeout(id) for timers, return () => observer.disconnect() for observers, return () => socket.close() for connections, or return unsubscribe if the subscribe call already gave you one.

Docs


useEffect(() => {
if (!ready) {
setStage(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-hooks-js/set-state-in-effect (warning)

This synchronous effect update causes an extra render: Calling setState synchronously within an effect can trigger cascading renders. Prefer deriving or initializing the value before render. If the effect must read a browser API after mount, treat this as advisory or suppress it with // react-doctor-disable-next-line react-hooks-js/set-state-in-effect.

Fix → Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

Docs

const [stage, setStage] = useState(0);
const ready = !isSettingsPending;

useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/effect-needs-cleanup (error)

setTimeout creates a timer in useEffect without guaranteed cleanup. Return a cleanup function that owns every allocation so it does not leak after unmount.

Fix → Return a cleanup function that stops the subscription or timer: return () => target.removeEventListener(name, handler) for listeners, return () => clearInterval(id) or clearTimeout(id) for timers, return () => observer.disconnect() for observers, return () => socket.close() for connections, or return unsubscribe if the subscribe call already gave you one.

Docs


useEffect(() => {
if (!ready) {
setStage(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-hooks-js/set-state-in-effect (warning)

This synchronous effect update causes an extra render: Calling setState synchronously within an effect can trigger cascading renders. Prefer deriving or initializing the value before render. If the effect must read a browser API after mount, treat this as advisory or suppress it with // react-doctor-disable-next-line react-hooks-js/set-state-in-effect.

Fix → Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

Docs

<ChartSeriesLegend
<InstrumentModule eyebrow={title} readout={readout}>
{hasData ? (
<Chart

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-hooks-js/static-components (error)

This component misses React Compiler's automatic memoization & re-renders more than it should: Cannot create components during render. Rewrite the flagged code so the compiler can optimize it.

Fix → Components created during render will reset their state each time they are created. Declare components outside of render.

Docs

engagement: Number(row.engagement),
};
}),
posts: (result?.data ?? [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

? Math.round((groundedRate - rawRate) * PERCENT)
: null;
const bestEngine =
[...engines].sort((a, b) => b.mentionRate - a.mentionRate)[0] ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-tosorted-immutable (warning)

This wastes work because [...array].sort() copies the array just to sort it, so use array.toSorted() to sort without the extra copy (ES2023)

Fix → Use array.toSorted() (ES2023) instead of [...array].sort() so you sort without copying the array first

Docs

}
try {
const files = await readdir(RESULTS_DIR);
const rounds = files

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .map().filter() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

const results: ScenarioResult[] = [];
for (const scenario of IRIS_EVAL_SCENARIOS) {
process.stdout.write(` ${scenario.id} ...\n`);
results.push(await runScenario(scenario));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This makes the for…of loop slow because each await runs one after another, so collect the independent calls & run them together with await Promise.all(items.map(...))

Fix → Collect the items, then use await Promise.all(items.map(...)) so independent work runs at the same time

Docs

engagement: row.engagement,
}));

const bestWeekdays = [...performance.data]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-tosorted-immutable (warning)

This wastes work because [...array].sort() copies the array just to sort it, so use array.toSorted() to sort without the extra copy (ES2023)

Fix → Use array.toSorted() (ES2023) instead of [...array].sort() so you sort without copying the array first

Docs

@comp-ai-code-review comp-ai-code-review 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.

No blocking issues found across the changed files.

Commit d9751ba · Posted by Comp AI Code Reviews.

@comp-ai-code-review comp-ai-code-review 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.

No blocking issues found across the changed files.

Commit df3d0d7 · Posted by Comp AI Code Reviews.

Remove the track button and dialog, drop the rank-change column, and pin the window select to the standard module header height so row lines align across modules.

@comp-ai-code-review comp-ai-code-review 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.

No blocking issues found across the changed files.

Commit 50839b4 · Posted by Comp AI Code Reviews.

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.

1 participant