All articles

Mobile compliance: does the publisher still have control?

On the web, a publisher roughly knows what its site does with data. You open the browser network tab, the requests and cookies are there in the clear, and a DPO can monitor them continuously. The web is the ground where control stays within reach.

Mobile is another game. Declared behavior and real behavior diverge, there is no network tab, TLS is pinned, and some SDKs encrypt their own content on top of TLS. A publisher embeds dozens of third-party SDKs it did not write, and has little, natively, to see what they actually do. Meanwhile, the French regulator has been inspecting mobile apps since 2025, and it is the publisher who answers for them.

Before even asking whether an app is compliant, a question for the DPO, there is a simpler one: does the publisher see, and control, what its app does once the user has refused? To make it concrete, we measured one. We took a major French news app, ran it on a real phone, refused consent on screen, and measured what leaves the device before and after that refusal. The app stays anonymous; the vendors are named, because it is their technical behavior that is observed. Identifiers and sensitive values are truncated.

Method and scope

The measurement covers a session of about two minutes, on a device running a recent version of Android. Three observation layers are combined. First the network traffic, captured in the clear after TLS decryption. Then app-level instrumentation that recovers the content of requests some SDKs encrypt themselves on top of TLS, and which therefore stays invisible to a plain network capture. Finally, terminal-access logging: reads of device identifiers and read or write operations in local storage.

Two limits to keep in mind. The session is a single run, so the figures describe one journey, not an average. And the only device-identifier source instrumented here is the advertising identifier; the findings on identifier reads are therefore limited to it.

The differential, in figures

The session counts 241 requests. 47 leave before any interaction with the banner, that is, before the user has made any choice at all. 194 leave after the explicit refusal.

Before any choice, ten vendor families are already contacted: Meta, Google, AppLovin, Smart AdServer, Ogury, the Batch engagement SDK, the Octopus Community comments SDK, and the Didomi consent platform. Part of the advertising and measurement chain initialises and emits before the user could accept or refuse anything. A first sign that firing is not driven by the choice.

After the refusal, traffic does not go down, it goes up.

Requests by vendor family, before any choice then after the refusal
vendor                     before   after
First-party                   10      46
Taboola                        0      45
Digiteka / Ultimedia           0      41
Batch (engagement/push)        4      23
Octopus Community              6      13
Meta                           9       9
Google                         8       9
Smart AdServer                 1       3
videoadex                      0       3
AppLovin                       3       0
Ogury                          1       0
Integral Ad Science            0       1
Didomi (CMP)                   3       1
Others (CDN, misc.)            2       0
total                         47     194

What the refusal does not change

Refusing cut none of these flows. First-party domains and video are expected for a news app; Taboola, Batch, Smart AdServer, videoadex and Integral Ad Science less so after a refusal. The behavior is almost the same on both sides of the banner.

Meta and Google stay active at the same level as before the choice. Taboola ramps up after the refusal, with forty-five requests and, this time, without a systematic "do not track" signal. Writes to terminal storage continue too: a local-storage write is information recorded on the device.

Local-storage writes, after the refusal
Batch ................. 21   (13 prefs, 8 database)
Facebook Analytics .... 15   (prefs)
Google AdMob ..........  2   (prefs)

The one identifier that does not leak

The advertising identifier is read by the app, nine times in the session. On each read, the system returns a null value, before and after the choice alike.

So neutralising the advertising identifier is not an effect of the refusal: it is a state of the device, handled at the system level and not by the app banner. The only protection signal that holds in this session is the one the platform enforces, not the one the publisher believes it drives. Note also what is not there: no request carries an IAB TCF consent string. The refusal is not propagated to vendors through that standardised channel, because it is not used here.

Reading the advertising identifier
14:54:06  source = advertising_id
          sdk    = com.google.android.gms.ads.identifier
          value  = 00000000-0000-0000-0000-000000000000  (36 chars, all zero)
          via    = AdvertisingIdClient$Info.getId(Native Method)

The Batch case, under the encryption layer

This is where the question of control becomes concrete. Requests to Batch leave with a body unreadable at the network level: the SDK encrypts its own content, on top of the TLS already in place. Its own headers announce it.

Headers of a Batch request (anonymised)
POST /ws/.../tracking
X-Batch-Content-Cipher: 2
X-Batch-Accept-Cipher: 2
X-Batch-Nonce: 5389076d-…-ed68e99
Content-Type: application/json

<body encrypted by the SDK, opaque to network capture>

Under the encryption, a persistent identifier

X-Batch-Content-Cipher: 2 signals app-level encryption specific to the SDK, above TLS. An analysis limited to the network capture stops here and classes the request as non-exploitable: the data leaves in a form the publisher itself does not read with its usual tools. By instrumenting the SDK, we recover the content before it is wrapped. In this session, fifty-four of these payloads were recovered in the clear after the refusal.

Once readable, the content shows first a persistent install identifier. The SDK states itself that the install dates from eight days before the capture: the identifier is stable over time and linkable from one session to the next. Note geoip: false, IP geolocation is disabled on the SDK side.

Install identifier sent to Batch (anonymised)
{
  "ids": {
    "di":  "e68c…fa16",            // install identifier
    "fda": "2026-06-10T07:43:47Z", // first install date
    "bid": "fr.****.****",
    "s":   "0a1b…acbc",            // session
    "data_collection": { "geoip": false }
  }
}

A profile attached to that identifier

Tied to the same identifier, a profile update. The first request to leave after the refusal carries a change to notification subscriptions: interest segments, attached to a persistent identifier, sent after a refusal, in a channel the publisher does not see without going to decrypt it.

Profile update sent after the refusal (anonymised)
{
  "name": "_PROFILE_DATA_CHANGED",
  "params": {
    "custom_attributes": {
      "push_topics.a": {
        "$add":    ["actu-generale", "infos-du-matin"],
        "$remove": ["sport"]
      }
    }
  }
}

What the measurement shows

Three things stand out. Part of the vendors are contacted before any interaction with the banner. The volume and the vendors do not decrease after the refusal, they increase, and writes to terminal storage continue. And a persistent identifier gets interest segments attached to it, after that same refusal, in a payload re-encrypted by the SDK.

None of these behaviors is visible with the tools that suffice on the web. The publisher embeds third-party SDKs it did not write, it answers for them before the regulator, but their real behavior is not observable without instrumenting the app in depth. Control presupposes visibility, and visibility is not there by default.

We stop at the observation. Whether these behaviors are compliant, which fall under an exemption, and how responsibility is shared, are legal questions: they belong to a DPO or to counsel, not to a technical measurement. What precedes is not a compliance opinion, it is a record.

Conclusion

On the web, the publisher has the dashboard: it sees what leaves and can monitor it. On mobile, the same publisher stays responsible for what its app does, but part of that behavior is invisible to it: firing before any choice, flows that do not stop at the refusal, and at least one SDK that maintains a persistent identifier and attaches a profile to it, all under an encryption layer you have to undo just to observe it.

So the question is not only "is this app compliant", but "does the publisher have the means to know". Acquiring those means is already part of preparing for an inspection. The gap between declared behavior and real behavior is the whole subject, and it only becomes measurable once you go down to the right level.