Skip to content

feat(meta-ads): add meta ads integration for campaign and ad performance queries#3563

Open
waleedlatif1 wants to merge 9 commits intostagingfrom
waleedlatif1/meta-ads-integration
Open

feat(meta-ads): add meta ads integration for campaign and ad performance queries#3563
waleedlatif1 wants to merge 9 commits intostagingfrom
waleedlatif1/meta-ads-integration

Conversation

@waleedlatif1
Copy link
Collaborator

Summary

  • Add Meta Ads integration with 5 tools: get account info, list campaigns, list ad sets, list ads, and get performance insights
  • Add account and campaign selectors with cascading dropdown support (select account → campaigns auto-populate)
  • OAuth configured with minimal ads_read scope for read-only operations
  • Auto-generated docs

Type of Change

  • New feature

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Mar 13, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 14, 2026 4:10pm

Request Review

@cursor
Copy link

cursor bot commented Mar 13, 2026

PR Summary

Medium Risk
Adds a new OAuth provider and token-exchange logic plus new tool endpoints that call the Meta Graph API; failures or misconfiguration could impact credential handling and new API routes, but changes are largely additive.

Overview
Adds a new Meta Ads integration end-to-end: a meta_ads block with OAuth auth, cascading selectors (ad account → campaign), and five new tools to fetch account details, list campaigns/ad sets/ads, and query insights/metrics from the Meta Marketing API.

Introduces supporting infrastructure: new Next.js API routes for selector dropdown data (/api/tools/meta_ads/accounts and /api/tools/meta_ads/campaigns), registers the tools in the tool registry, and wires up a new OAuth provider (meta-ads) including env vars, ads_read scope metadata, and post-connect exchange of short-lived tokens to long-lived tokens.

Updates docs and UI assets by adding MetaAdsIcon, mapping meta_ads to the icon, and generating a new meta_ads docs page (plus a small Grain docs update).

Written by Cursor Bugbot for commit b2bd567. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR adds a full Meta Ads integration with 5 read-only tools (get account info, list campaigns, list ad sets, list ads, get performance insights), OAuth via the Facebook Marketing API, cascading account/campaign dropdowns, and auto-generated docs. The tool implementations, block config, selector registry, and OAuth provider wiring are generally well-structured and follow existing patterns. However, there is a critical bug in the OAuth getUserInfo handler that will cause a new database record to be created on every login.

Key changes:

  • New apps/sim/tools/meta_ads/ directory with 5 tool definitions and shared types/utilities
  • apps/sim/blocks/blocks/meta_ads.ts — block config with cascading project-selector dropdowns for account and campaign
  • apps/sim/lib/auth/auth.ts — long-lived token exchange on sign-in + new meta-ads OAuth provider definition
  • apps/sim/app/api/tools/meta_ads/accounts/route.ts and campaigns/route.ts — selector-population API routes
  • apps/sim/hooks/selectors/registry.tsmeta-ads.accounts and meta-ads.campaigns selector definitions

Issues found:

  • Critical: getUserInfo in auth.ts appends crypto.randomUUID() to the Facebook user ID, creating a new account row on every login and breaking token persistence/re-linking.
  • Logic: Optional META_ADS_CLIENT_ID and META_ADS_CLIENT_SECRET env vars are cast with as string inside the token exchange block, which will silently pass the string "undefined" to Facebook's API if those vars are unset.
  • Logic: The selector API routes hard-code limit=200 with no pagination, silently truncating users or agencies with more than 200 ad accounts/campaigns.
  • Style: When both adSetId and campaignId are supplied to get_insights or list_ads, campaignId is silently discarded without any warning or documentation.

Confidence Score: 1/5

  • Not safe to merge — the getUserInfo UUID bug will create a new credential record on every Meta Ads login, corrupting the auth database over time.
  • The core OAuth flow has a critical correctness bug: generating a random UUID suffix for the provider user ID on every getUserInfo call defeats better-auth's account deduplication. Every login creates a fresh account row, meaning stored access tokens are orphaned after the first re-authentication. This must be fixed before merging. The rest of the feature (tools, block config, selectors) is well-implemented and follows established project patterns.
  • apps/sim/lib/auth/auth.ts requires the most attention due to the non-stable user ID in getUserInfo and the unsafe as string cast on optional env vars.

Important Files Changed

Filename Overview
apps/sim/lib/auth/auth.ts Critical bug: getUserInfo appends crypto.randomUUID() to the Meta user ID, creating a new account record on every login and preventing credential deduplication. Additionally, optional env vars are unsafely cast to string.
apps/sim/tools/meta_ads/get_insights.ts Well-structured insights tool with proper Meta API field mapping and conversion action aggregation; adSetId silently overrides campaignId as parent entity without documentation.
apps/sim/app/api/tools/meta_ads/accounts/route.ts Correctly validates credentials and uses refreshAccessTokenIfNeeded, but hard-codes limit=200 with no pagination handling, which silently truncates users with many ad accounts.
apps/sim/app/api/tools/meta_ads/campaigns/route.ts Same hard-coded limit=200 issue as accounts route; otherwise follows the established pattern for selector API routes.
apps/sim/blocks/blocks/meta_ads.ts Well-structured block config with cascading dropdowns, canonicalParamId mappings, and proper basic/advanced mode handling; follows existing block patterns.
apps/sim/lib/oauth/oauth.ts Properly registers the meta-ads provider with minimal ads_read scope; getProviderAuthConfig correctly uses getCredentials helper instead of raw casts.
apps/sim/tools/meta_ads/types.ts Clean type definitions and helper utilities; getMetaApiBaseUrl and stripActPrefix are well-designed and reused consistently across tools.
apps/sim/hooks/selectors/registry.ts Correct cascading selector implementation; meta-ads.campaigns query key includes both oauthCredential and accountId so it refetches when the upstream account selection changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User clicks Connect Meta Ads] --> B[OAuth redirect to Meta\nads_read permission]
    B --> C[User grants permission]
    C --> D[auth.ts linkAccount callback]
    D --> E[Exchange short-lived token\nfor long-lived token via fb_exchange_token]
    E --> F[Store long-lived token in DB]

    F --> G[User opens Meta Ads block]
    G --> H[accounts route:\nGET /me/adaccounts - limit=200]
    H --> I[Account dropdown populated]
    I --> J[User selects account]
    J --> K[campaigns route:\nGET /act_id/campaigns - limit=200]
    K --> L[Campaign dropdown populated]

    L --> M{User selects operation}
    M -->|get_account| N[GET /act_id]
    M -->|list_campaigns| O[GET /act_id/campaigns]
    M -->|list_ad_sets| P[GET /campaign_id or act_id/adsets]
    M -->|list_ads| Q[GET /adset_id or campaign_id or act_id/ads]
    M -->|get_insights| R[GET /parentId/insights\nlevel + date_preset]

    N & O & P & Q & R --> S[transformResponse maps\nsnake_case to camelCase]
    S --> T[Output returned to workflow]
Loading

Comments Outside Diff (4)

  1. apps/sim/lib/auth/auth.ts, line 1085 (link)

    Non-stable user ID causes duplicate accounts on every login

    The id field in getUserInfo is supposed to return the provider's stable user identifier so that better-auth can look up the existing account record on subsequent logins. Appending crypto.randomUUID() generates a brand-new ID on every authentication, which will:

    1. Create a new account row in the database on every login (no deduplication).
    2. Prevent the existing stored access token from being found/updated on re-login.
    3. Accumulate orphaned credential records over time.

    The fix is to use only the Facebook user ID:

  2. apps/sim/lib/auth/auth.ts, line 1006-1007 (link)

    Optional env vars unsafely cast to string

    env.META_ADS_CLIENT_ID and env.META_ADS_CLIENT_SECRET are declared as z.string().optional(), so they can be undefined. Casting them with as string does not narrow away undefined—at runtime these will evaluate to the string "undefined" if the env vars are not set, causing the token exchange to silently send invalid credentials to Facebook.

    The same pattern occurs at apps/sim/lib/auth/auth.ts in the provider definition (lines 1053–1054) as well as in apps/sim/lib/oauth/oauth.ts (the getProviderAuthConfig case which uses getCredentials helper and thus is safer).

    Consider guarding this block:

    Or better, add an early-out if credentials are missing before attempting the exchange:

    if (!env.META_ADS_CLIENT_ID || !env.META_ADS_CLIENT_SECRET) {
      logger.warn('Meta Ads client credentials not set; skipping token exchange')
      return
    }
  3. apps/sim/app/api/tools/meta_ads/accounts/route.ts, line 408 (link)

    Hard-coded limit=200 may silently truncate accounts

    The URL fetches at most 200 ad accounts in a single request:

    /me/adaccounts?fields=account_id,name,account_status&limit=200
    

    Users or agencies with more than 200 ad accounts will receive an incomplete list with no indication that results are truncated. The Meta Graph API returns a paging cursor in the response when there are more results (data.paging.cursors). Consider either checking for and following the cursor, or at minimum surfacing a hasMore field in the response.

    The same concern applies to the campaigns route at apps/sim/app/api/tools/meta_ads/campaigns/route.ts line 524, which also hard-codes limit=200.

  4. apps/sim/tools/meta_ads/get_insights.ts, line 98-107 (link)

    campaignId silently ignored when adSetId is also provided

    When both adSetId and campaignId are non-empty, only adSetId is used as the parent entity for the insights URL and campaignId is dropped without any warning. If the supplied campaignId belongs to a different account or ad set, the user will get results they didn't intend with no error or indication that campaignId was ignored.

    The same pattern appears in list_ads.ts at lines 67–74.

    Consider either documenting this precedence explicitly or logging a debug message, e.g.:

    if (params.adSetId) {
      parentId = params.adSetId.trim()
      // campaignId is ignored when adSetId is present – adSetId is more specific
    } else if (params.campaignId) {

Last reviewed commit: b2bd567

…nce queries

- Add 5 tools: get_account, list_campaigns, list_ad_sets, list_ads, get_insights
- Add account and campaign selectors with cascading dropdown support
- Add OAuth config with ads_read scope
- Generate docs
…um date preset to docs

- Pass access token via Authorization header instead of URL query param in getUserInfo, matching all other providers
- Add missing 'maximum' date preset to tool param description and docs
@waleedlatif1 waleedlatif1 force-pushed the waleedlatif1/meta-ads-integration branch from f11f141 to bb8b314 Compare March 14, 2026 13:28
@waleedlatif1
Copy link
Collaborator Author

Addressing Review Comments

1. crypto.randomUUID() in getUserInfoNot a bug

Every OAuth provider in the codebase uses this exact same ${profile.id}-${crypto.randomUUID()} pattern (Google, Google Ads, Slack, LinkedIn, Asana, etc.). This is an established convention in auth.ts, not a defect specific to Meta Ads.

2. Access token in URL query parameter — Fixed (893bc40)

Moved to Authorization: Bearer header in the getUserInfo fetch call, matching every other provider in the codebase.

3. maximum date preset undocumented — Fixed (893bc40)

Added maximum to the tool's param description in get_insights.ts and to the generated docs in meta_ads.mdx.

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

- Use useId() for MetaAdsIcon SVG gradient IDs to prevent collisions when multiple instances render on the same page
- Filter conversions to only count actual conversion action types (offsite_conversion, onsite_conversion, app_custom_event) instead of summing all actions
@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile review

…rity, account statuses, DELETED filter

- Add stripActPrefix() helper to prevent act_ double-prefix bug when users provide prefixed IDs
- Clarify totalCount descriptions to indicate response-level count (not total in account)
- Show all ad accounts in selector with status badges instead of silently filtering to active only
- Add DELETED to status filter dropdown options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

All 4 issues from the Greptile review have been addressed in b339b48:

1. act_ prefix double-prepend bug — Fixed. Added stripActPrefix() helper in types.ts that strips act_ if present before re-prepending. Applied across all 5 tool files and the campaigns API route.

2. Misleading totalCount field — Fixed. Updated descriptions in all 4 list/insights tools to clarify it's the response-level count: "Number of X returned in this response (may be limited by pagination)".

3. Accounts selector silently excludes non-active accounts — Fixed. Now shows all accounts regardless of status. Non-active accounts get a status badge appended to their name (e.g., "My Account (Disabled)", "My Account (Pending Risk Review)").

4. DELETED missing from status dropdown — Fixed. Added DELETED option to the status filter dropdown in the block config.

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile review

…th connect

Meta's auth code flow returns a short-lived token (~1-2h) with no refresh token.
Add fb_exchange_token call in account.create.after hook to exchange for a
long-lived token (~60 days), following the same pattern as Salesforce's
post-connect token handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s in conversions

The conversion filter was only matching offsite_conversion.* subtypes but
missing onsite_conversion.* and app_custom_event.* subtypes, which the Meta
API commonly returns at the subtype level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@waleedlatif1
Copy link
Collaborator Author

@greptile

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