I tried measuring ads in React Native — there was a lot more to it than I thought

I spent a while debugging a viewability bug that made no sense.

The ad was clearly visible on screen. The OMID session was active. The impression event was firing. The native view had a valid size. Nothing looked obviously broken in React Native.

But verification kept reporting:

0% viewability

The problem was not the ad creative. It was not the impression event. It was not even obviously the OM SDK setup.

The problem was the native view hierarchy.

The Setup

We were building React Native support for OMID using the native Android and iOS OM SDKs because no existing solution matched our requirements.

The idea looked simple:

  1. Create an OMID session from JavaScript.
  2. Resolve a React Native view tag to a native Android view.
  3. Register that view with the native OM SDK.
  4. Fire loaded, impression, and media events from JS.

At a high level, the Android integration looked like this:

val view = resolveView(reactTag)
adSession.registerAdView(view)
adSession.start()
adEvents.impressionOccurred()

That sounds reasonable. OMID needs a native view. React Native can give us a native view. Done.

Except it was not done.

This was not just a JavaScript API problem. Once we crossed into OMID, the native view hierarchy became part of the product.

The symptom

In React Native, our OMID integration consistently reported 0% viewability.

The confusing part: the same screen also had AdMob ads using react-native-google-mobile-ads, and those ads did not suffer from the same issue.

So the first question was obvious:

What is AdMob doing that our integration is not?

At first, I assumed maybe Invertase had some special React Native handling. It does not appear to. The library mostly creates Google’s native AdView, AdManagerAdView, or NativeAdView, adds it to the React Native hierarchy, and lets the Google Mobile Ads SDK do the rest.

That was the important clue.

What I checked first

I went through the usual suspects:

  • Was OMID activated? Yes.
  • Was the session created correctly? Yes.
  • Was I registering the native ad view? Yes.
  • Was the view attached to the window? Yes.
  • Did it have non-zero width and height? Yes.
  • Was the impression event firing before layout? I delayed registration until after layout. Still bad.
  • Was I passing the wrong React tag? I dumped the resolved view. It was the expected native view.

At this point, Everything appeared correct from our integration’s point of view.

So I stopped looking at the React component tree and started looking at the Android view hierarchy.

The weird finding

The registered ad view was visible. But above it, React Native had inserted a transparent, full-screen framework view.

Visually, that view did not matter. The user could still see the ad.

The important detail was that this view was only visually transparent. From Android’s point of view, it was still a real visible view: it had normal visibility, a non-zero alpha value, and full-screen bounds.

But OMID does not measure “what I think the UI looks like.” OMID measures native view geometry, visibility, and obstruction state.

A visually transparent native view can still be a native view sitting above the ad. If that view is outside the registered ad view hierarchy, has a visible state, and is not registered as a friendly obstruction, OMID has a genuine reason to treat it as blocking the ad.

That is how you can end up with this absurd-looking result:

Ad visible to human: Yes
Ad view attached and laid out: Yes
OMID viewability: 0%

OMID does not simply ask whether a view is visually transparent. It evaluates the native view hierarchy and treats overlapping views as potential obstructions. A fully transparent framework view can still appear as an overlapping native view during measurement, causing visibility calculations to drop to zero.

Why React Native makes this easy to miss

React Native hides a lot of native view complexity. As app developers, we usually think in terms of JSX:

<View>
  <Ad />
</View>

But Android sees a much more complicated hierarchy underneath:

DecorView
  ReactRootView
    ReactViewGroup
      SomeFrameworkContainer
      SomeTransparentOverlay
      YourAdContainer
        YourAdView

Depending on your architecture, navigation, portals, modals, gesture handlers, Expo wrappers, debugging layers, or Fabric behavior, there may be native views you never explicitly rendered.

They may be transparent. They may not draw anything. But they still matter to OMID.

Why AdMob did not break the same way

This was the part that initially confused me.

I was using react-native-google-mobile-ads in the same app, and AdMob viewability was fine. After inspecting the Google Mobile Ads SDK artifacts, the difference became clearer.

AdMob does not simply expose a native view and expect app code to wire OMID manually. Google’s SDK owns the measurement stack.

For native ads, NativeAdView creates an internal full-size overlay FrameLayout, keeps it above its children, and passes both the outer native ad container and this internal overlay into a hidden native ad view delegate. It also has structured knowledge of the ad’s registered asset views and the native ad object itself.

So Google’s SDK knows much more than our first-pass integration knew:

  • The actual native ad container
  • The registered asset views
  • The media view and AdChoices view
  • Its own internal overlay views and measurement WebViews
  • The OMID session lifecycle

The conclusion:

Mature native ad SDKs manage OMID with more native context than an integration that naively passes a single native view to OMID. That context matters.

The important distinction

Naive integration approach Mature native SDK approach
React Native view ref → native View → OMID registerAdView(view) SDK-owned ad container + registered asset views + SDK internal overlays + SDK measurement WebView + SDK OMID session + SDK obstruction rules

Both use native views, but they do not give OMID the same amount of context.

That difference can decide whether a transparent framework view becomes harmless or fatal to viewability.

Friendly obstructions are not a magic fix

OMID supports friendly obstructions. That means you can tell OMID:

This view is covering the ad, but it is expected and should not count as a bad obstruction.

Common examples include:

  • Close buttons
  • AdChoices icons
  • Video controls
  • Non-ad-blocking overlays required by the ad experience

But this is not something to use casually.

You should not mark arbitrary app content as friendly. You should not whitelist a real overlay that actually blocks the ad from the user; that defeats the purpose of measurement.

In my case, the interesting category was framework-owned transparent views. They were visually non-blocking, but native-measurement-visible.

That is where a React Native OMID integration needs a strategy.

Debugging checklist

If OMID reports 0% viewability in React Native but the ad looks perfectly visible, start simple: check the native view hierarchy.

1. Confirm the registered ad view is actually ready

Check:

view.isAttachedToWindow
view.width
view.height
view.visibility
view.alpha

2. Dump the parent chain of the registered view

For example:

AdView -> Parent -> Parent -> ReactRootView -> DecorView

3. Inspect siblings and window-level children around the ad

Look closely for:

  • Full-screen transparent ViewGroups
  • Expo framework wrappers
  • Portal, modal, or debug overlays
  • Views outside the registered ad hierarchy
  • Anything with normal visibility, non-zero alpha, and bounds covering the ad

Android Studio Layout Inspector is the easiest way to validate this.

A small Logcat dump of class name, size, position, visibility, and alpha is usually enough to spot the suspicious view.

Mitigations

The most important mitigation is to register the correct native view.

Do not blindly register the smallest leaf view. Prefer the native container that truly represents the ad.

Additionally:

  • Wait until the view is attached and layout has completed.
  • Ensure width and height are non-zero.
  • Re-check after navigation transitions.
  • Register legitimate friendly obstructions explicitly.
  • Avoid marking real content overlays as friendly.
  • Test in Expo and bare React Native separately.
  • Test the old architecture and Fabric separately if you support both.

The takeaway

This bug changed how I think about React Native ad measurement.

Building OMID support for React Native is not just about exposing native methods to JavaScript. The bridge is the easy part.

The hard part is making sure the native view you register represents the exact same visual reality that OMID will measure. In React Native, those two things can easily drift apart.

The ad can be visible to the user, valid in JSX, attached in Android, and still measure as 0% because a transparent native framework view sits directly above it.

So if you are building OMID support for React Native, do not stop at:

adSession.registerAdView(view)

You need a native view-hierarchy strategy.

That is where the real integration work begins.

Leave a reply:

Your email address will not be published.

Site Footer

Sliding Sidebar