← All articles
7 min readTelemetry,Consent,Privacy,UX

How Flowo Asks for Telemetry Consent, in the App and on the Web

By Maksym Bardakh Ivko · Chief Executive Officer

In short

In Flowo, the planning app from BBMM Technologies, no analytics SDK starts when the app launches. One service starts them, and only after the consent ledger holds a granted decision at the current policy version. Every event then passes an allowlist that drops unknown names and content fields, and the website stores a versioned choice that expires after 180 days.

Nothing starts at cold start

Flowo starts no analytics SDK during launch. The first startup step is ComplianceService.bootstrap, which builds the encrypted storage and the consent manager before any data box opens. Code that reaches for the compliance service earlier gets a StateError, so there is no quiet fallback to unprotected defaults.

A single class, TrackingActivationService, is allowed to start Amplitude and TelemetryDeck. On each launch it reads the ledger and starts them only if the stored analytics decision is granted and current. For a new install it does nothing until the consent screen reports a choice.

We put the decision in one place because scattered checks drift. A feature that calls an SDK directly would bypass consent without anyone noticing, so features call a facade and the facade has nothing to send to until the gate opens.

How the consent ledger works

The consent ledger is an append-only list of decisions kept in an encrypted box on the device. A new decision is added as a new record and the earlier ones stay, so the history of what a person agreed to is preserved.

  • purpose: which use of data the decision covers
  • granted: true for a grant, false for a refusal or withdrawal
  • version: the consent version in force when the decision was made
  • timestamp: the moment of the decision in UTC
  • lawfulBasis: the legal basis recorded with it

A purpose counts as current only when its latest record is granted and its version equals the app's consent version, which is 3 today. Raising that number makes every older grant stale, and the consent screen appears again. Each grant and withdrawal also writes an entry to a hash-chained audit log.

What the consent screen asks

The screen asks only about purposes a person can refuse. Analytics and session replay are switches. Essential storage and cloud AI are shown as explanations with no switch, because neither rests on consent and a toggle would promise a choice that does not exist.

Version 2 of the screen removed two questions. We stopped asking about crash reports because no crash SDK ships in the app, and about personalization because that data never leaves the device. Voice upload is asked at the first recording, since that is the moment the question means something.

Every optional switch starts off for a new install. If the ledger cannot be read, the screen shows all optional purposes as off. Session replay depends on analytics, so turning analytics off turns replay off with it. A decline button clears every optional switch in one tap.

What an event may contain

Every analytics event passes through AnalyticsEventPolicy before any provider sees it. The policy is an allowlist, so an event the code did not register in advance is rejected.

  • The event name must be on the allowlist, and each property must be allowed for that event.
  • Values may be booleans, numbers, strings up to 128 characters, or lists of up to 20 strings. Nested objects are rejected.
  • An event may carry at most 64 properties.
  • Keys such as title, notes, prompt, message and transcript are refused outright.
  • Task and habit identifiers are replaced with their SHA-256 hash.
  • A category name is reduced to built_in or custom.

A rejected event is dropped. The log line records the event name and the error type and leaves out the values, because a rejected value may itself be sensitive.

The same contract is checked in CI. A script, validate_analytics_governance.py, compares a version-controlled manifest with the event constants and the policy in the Dart source, and the build fails when the two disagree.

The Amplitude configuration turns off IP address, city, country, carrier, device model and advertising identifier collection. Identity is a random UUID generated on first install. Because no advertising identifier is read, the app does not link the App Tracking Transparency framework.

When session replay can run

Session replay in the app needs four conditions at once, and analytics consent alone is never enough.

  1. 1.Analytics is granted and the SDKs have started.
  2. 2.The separate session_replay purpose is granted at the current version.
  3. 3.The build was compiled with ENABLE_SESSION_REPLAY=true.
  4. 4.The build carries a sample rate above zero.

When replay does run, it uses the SDK's conservative masking level and remote configuration is disabled. A change in a vendor dashboard therefore cannot loosen masking on devices already in use.

How the website banner works

On flowoapp.com the choice is stored in the browser as a small record with a policy version and a save time, plus one boolean per category. A stored choice is honoured only while its policy version matches the site's and it is younger than 180 days. Otherwise it reads as no choice and the banner returns.

We bump the policy version when a new processor joins a category. Choices saved under the previous storage key are deleted and are never migrated.

The banner offers Accept all and Reject as two buttons of the same size and style, side by side. A third control opens switches for analytics and for marketing measurement, both off by default.

If the browser sends Do Not Track or Global Privacy Control, both switches are disabled and the saved value is false even when the visitor presses Accept all. Third-party scripts load only on the live domain and only after the matching category is on. Preview and local builds send nothing.

What withdrawal does

Withdrawing consent stops collection in the same session. In the app, a withdrawal is written to the ledger and the activation service stops both SDKs. If a provider reports an error while stopping, the gate still closes.

Consent changes are applied one after another, so fast toggling cannot leave an SDK running against the final decision.

On the website, switching analytics off opts Amplitude out and deletes its storage keys and cookies. Switching marketing off revokes the pixel's consent and clears its two first-party cookies.

Key takeaways

  • Flowo starts Amplitude and TelemetryDeck only through TrackingActivationService, after a granted and current analytics decision exists in the ledger.
  • A consent decision is current only if its version equals the app's consent version, which is 3, so a policy change asks everyone again.
  • AnalyticsEventPolicy rejects unknown event names, nested values, strings over 128 characters and more than 64 properties.
  • Task and habit identifiers leave the device only as SHA-256 hashes, and content keys such as title and notes are refused.
  • The flowoapp.com consent record expires after 180 days and is discarded when the policy version changes.
  • Do Not Track and Global Privacy Control force analytics and marketing off on the website, including when Accept all is pressed.

Frequently asked questions

Does Flowo collect analytics before I answer the consent screen?
No. The app starts no analytics SDK at launch. Amplitude and TelemetryDeck are started by one service, and only after the consent ledger holds a granted analytics decision at the current consent version.
What happens to my consent when the privacy policy changes?
Each stored decision carries the consent version it was made under. When BBMM Technologies raises that version, older grants stop counting as current and the consent screen is shown again. The earlier records stay in the ledger as history.
Can Flowo analytics include my task titles or notes?
The event policy refuses property keys such as title, notes, prompt, message and transcript, and it rejects any event or property that is not on its allowlist. Task and habit identifiers are replaced with SHA-256 hashes before an event is sent. A rejected event is dropped.
How long does a cookie choice last on flowoapp.com?
A saved choice is valid for 180 days and only while the site's policy version is unchanged. After that the stored record is ignored and the banner asks again. Old records are deleted and never carried forward.
Does flowoapp.com respect Global Privacy Control?
Yes. When the browser sends Global Privacy Control or Do Not Track, the analytics and marketing switches are disabled and saved as off. Pressing Accept all does not change that.

References

About the author

Maksym Bardakh Ivko

Chief Executive Officer

Maksym leads product at BBMM Technologies. He designs Flowo’s scheduler and writes most of what ships in it.