Skip to content

feat: ad tracking API#1750

Open
peterporfy wants to merge 8 commits into
mainfrom
ads-220
Open

feat: ad tracking API#1750
peterporfy wants to merge 8 commits into
mainfrom
ads-220

Conversation

@peterporfy
Copy link
Copy Markdown

@peterporfy peterporfy commented May 11, 2026

Summary

Adds five new methods to track ad lifecycle events through RevenueCat:

  • Purchases.trackAdDisplayed(AdDisplayedData)
  • Purchases.trackAdOpened(AdOpenedData)
  • Purchases.trackAdLoaded(AdLoadedData)
  • Purchases.trackAdRevenue(AdRevenueData)
  • Purchases.trackAdFailedToLoad(AdFailedToLoadData)

All five methods are flat static methods on Purchases - following how it looks like on PHC.

Key decisions

Flat separate data classesAdDisplayedData, AdOpenedData, AdLoadedData, AdRevenueData, AdFailedToLoadData are independent types rather than a shared base class. They mirror the iOS and Android SDK design: identical fields today, but kept separate for semantic value.

Typed wrappersAdMediatorName, AdFormat, AdRevenuePrecision are open-ended value types with predefined constants, not enums. Matches the native SDK pattern, since ad networks and formats aren't a closed set.

trackAdFailedToLoad has no impressionId — consistent with PHC: a failed load doesn't produce an impression, so the field is omitted and mediatorErrorCode is added instead.

Testing

  • API surface covered by api_tester (compile-time shape checks)
  • Method channel wiring covered by unit tests
  • Manually tested with a companion Flutter sample app (google_mobile_ads, all 5 ad formats)

Note

Medium Risk
Adds new public API surface and native method-channel wiring on iOS/Android, so mismatched argument shapes or platform availability could cause runtime issues despite added tests; no changes to purchase flows or sensitive auth/data handling.

Overview
Adds an experimental ad tracking API exposed as Purchases.adTracker with five methods to report ad lifecycle events (trackAdDisplayed, trackAdOpened, trackAdLoaded, trackAdRevenue, trackAdFailedToLoad) using new typed payload models (e.g. AdRevenueData, AdFormat, AdMediatorName).

Wires these calls through the method channel on Android (PurchasesFlutterPlugin.java) and iOS (PurchasesFlutterPlugin.m, gated to iOS 15+/equivalents with warning logs), and makes them no-ops on web.

Updates exports/dependencies (meta) and adds coverage via api_tester compile-time API checks plus unit tests asserting the method names/argument maps sent over the channel.

Reviewed by Cursor Bugbot for commit cf7b774. Bugbot is set up for automated code reviews on this repo. Configure here.

Adds public API and model types for tracking ad lifecycle events:
- AdMediatorName, AdFormat, AdRevenuePrecision typed wrappers with predefined constants
- AdDisplayedData, AdOpenedData, AdLoadedData, AdRevenueData, AdFailedToLoadData event data classes
- 5 static methods on Purchases: trackAdDisplayed, trackAdOpened, trackAdLoaded, trackAdRevenue, trackAdFailedToLoad
- Method channel tests verifying correct method names and map serialization
Wires trackAdDisplayed, trackAdOpened, trackAdRevenue, trackAdLoaded,
and trackAdFailedToLoad through to RCCommonFunctionality. All methods
guarded with @available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0).
iOS and macOS share the same .m file (hard link).
Wires up the 5 trackAd* methods on Android (CommonKt calls), adds
no-op cases on web, and covers all new public API in api_tester.
@peterporfy peterporfy requested a review from a team as a code owner May 11, 2026 12:34
@peterporfy peterporfy marked this pull request as draft May 11, 2026 12:36
@peterporfy peterporfy marked this pull request as ready for review May 11, 2026 12:48
@peterporfy peterporfy requested a review from polmiro May 11, 2026 12:53
Copy link
Copy Markdown
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

Looking great! Nothing too blocking, just some thoughts. Let me know what you think!

}

void _checkTrackAdDisplayed() {
Future<void> future = Purchases.trackAdDisplayed(const AdDisplayedData(
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.

I know this is in line with what we're doing in other APIs, but to avoid bloating the Purchases API, I think it would be ideal to keep the ad methods within a separate object, like we're doing in the natives, even if they remain static methods, so these can be for example something like:

  • Purchases.AdTracker.trackAdDisplayed (in a namespace if possible) or
  • PurchasesAdTracker (if not possible to namespace).

I understand this is a bit trickier... But I think it could help to avoid some bloating issues. I also think it's fine even if internally we need to reuse the same channel to communicate with the kotlin/swift layer. Lmk what you think! 🙏

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I agree that it would be more in line with existing SDKs. Although Purchases.AdTracker won't work I think, but we can do a singleton Purchases.adTracker on the static Purchases - or a separate static PurchasesAdTracker as you suggested. I am leaning towards the second. Wdyt?

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.

IMO, we should avoid having static APIs... I think we have this most for legacy reasons... So I kinda would lean towards the singleton approach if possible. Feels to me that it's easier to test and iterate on 🙏

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated to singleton - indeed looks better

import 'ad_format.dart';
import 'ad_mediator_name.dart';

class AdDisplayedData {
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.

Just to check, these are not experimental (we don't currently have a good way to mark these as experimental in dart). Just to confirm, but are we ok with launching these as stable? And if so, should we make the native APIs stable already?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That's a good question, I will check with the team.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

What if we don't document these features in flutter/unity/reactnative etc until it is not marked as stable in the core sdks? Would that be good enough to signal they are experimental/not official yet? Or we must have it stable in ios/android before we add anything here? Wdyt?

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.

Even if we don't publicize them, if we release these APIs in a release as stable, I think we should consider them stable and basically, we should try very very hard to avoid breaking changes, or they would need to be a major.

So I think the proper way is to use meta's library and add an experimental annotation to these APIs: https://api.flutter.dev/flutter/meta/experimental-constant.html. We haven't used this before but I think it's probably worth using it here...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Oh that's nice - I've added the annotation, thanks!

Comment thread lib/models/ad_displayed_data.dart Outdated
Copy link
Copy Markdown

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

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

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit e47ff53. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants