whisperr 0.8.0
whisperr: ^0.8.0 copied to clipboard
Official Whisperr SDK for Flutter — identify users and track product events to power churn-prevention interventions.
Changelog #
0.8.0 #
- Sessions. Every event carries
context.session_id(sess_plus a UUID v4): new on each cold start and after 30 minutes without an event. app_backgroundedcounters:session_id,screen_count,unique_screen_count,event_count,error_count.foreground_msnow uses a monotonic clock.launch_sourceonapp_opened(push,deep_link,universal_link,widget,shortcut,email,direct).app_openedwaits up to 1500 ms (WhisperrOptions.launchSourceWindow) for a push tap or a link passed to the newhandleLaunchUri();flush()and going to the background end the wait. Links addlink_route(path only, ids redacted),link_host,utm_*;first_after_updatemarks the first open after an update.WhisperrNavigatorObserversendsscreen_viewedfor named page routes.app_error: uncaught errors throughFlutterError.onErrorandPlatformDispatcher.instance.onError(chained; the previous handlers still run), plustrackError(). Type name and fingerprint only, never the message or stack. Deduped per fingerprint, at most 20 per session.WhisperrOptions.captureErrors(default true) turns automatic capture off.app_crashedon the next launch when the app ended while in the foreground (unclean-shutdown flag). Never in debug builds.
0.7.0 #
emailOptedInonidentify(). Your user's email consent, sent asopted_inon theemailshortcut. Whether it opts the address in depends on the app's answer to "Does your signup get permission to email users?" in Whisperr.- Behavior change: the
emailshortcut no longer sendsopted_in: trueon its own. WithoutemailOptedInit sends noopted_in. Versions before 0.7.0 sendopted_in: trueon every email shortcut. Push and phone are unchanged. - An email channel with
opted_in: truealso sendsconsent_source: "user". The server needs it to grant consent in "Per user" mode, which tells this apart from thetruethat older versions sent by default. - An identify from the app no longer opts an address back in after the user
opted out of it (including through the unsubscribe link). This rule is
enforced by the Whisperr backend, which also has to accept
consent_sourcebefore this version ships.
0.6.1 #
- Rejected requests warn without debug. When the server rejects a request
with a non-retryable
4xx(not401,403or429), the SDK drops it, as before, and now logs adebugPrintwarning the first time each status code occurs, even withdebugoff. The message names only the status code. Every drop still reachesonError; withdebugon, every drop is logged as before.429and5xxkeep retrying.
0.6.0 #
Contract v2 (whisperr-spec SPEC.md "Contract v2"). The server must accept
contract v2 before apps ship this version: an older server rejects the
anonymous identify and the new push token fields with 400.
-
Restore identify at start. With a stored user and no opt-out, each start queues one identify:
external_user_id, the storedanonymous_id, and the traitslocale, the timezone andapp_language. No channels. It waitsWhisperrOptions.restoreIdentifyDelay(3 seconds;Duration.zeroqueues it duringstart()). The app's ownidentify()in that time replaces it. It is skipped when the same traits went out for the same user less than 24 hours ago, and when one is still queued (offline). It uses the ordered, persisted queue. -
setAppLanguage(String?)sets the language the user picked in the app, a BCP 47 tag._becomes-(Locale.toString()works). The SDK stores it and sends the traitapp_languageat once: a partial identify with a user, an anonymous identify (anonymous_idonly) without one. Everyidentify()and restore identify carries it; atraits['app_language']from the caller wins. The same value again sends nothing.nullclears the stored value and sends nothing. An invalid tag is ignored.reset()keeps it. New getterappLanguage. -
Anonymous push token.
setPushTokenbeforeidentify()now sends the token at once in an anonymous identify (anonymous_idand the push channel). Before, the SDK held it in memory for the nextidentify(). The dedupe pair is now (user or anonymous id, token).identify()attaches a token registered under the anonymous id. A permissiondeniedbefore login opts the anonymous token out;grantedopts it in under the anonymous id.optOut()opts an anonymous token out under the anonymous id. -
reset()moves the push token. With a registered token,reset()opts it out under the old user, rotates theanonymous_id, and opts it in under the new one. You no longer callsetPushTokenafter logout. A full queue does not fail the logout (the move is skipped, with anonErrorreport). A token held whiledeniedstays held acrossreset(). -
Push token fields. Every opted-in push entry carries
platform(also for a bare token and foridentify(pushToken:)) and the newclient_app_id(bundle id or package name, frompackage_info_plus).setPushTokenandattachPushTokenStreamtakeproviderProject(the Firebase project id for FCM), sent as the newprovider_project.provider_projectnever goes with an APNs token;push_envgoes only with APNs tokens (and with a token of unknown kind).WhisperrChannel.pushtakesclientAppIdandproviderProject. -
7-day re-registration. The stored pair keeps its send time. The SDK sends the same token again once it was sent 7 days ago or more: at launch, or on the next
setPushToken. A pair stored by 0.5.x has no send time, so the first launch of 0.6.0 sends the token once more, now with the new fields. -
Metadata dedupe. A token whose fields are all already in the sent entry (for example a bare token after an FCM token) is not sent again. Before, a bare token after a token with fields was deduped only when it had no fields at all.
-
push_openedcarriesaction_id(actionId:;defaultfor a tap on the notification) andcold_startwhen the caller passescoldStart:. -
trackPushReceived(data)sends the new automatic eventpush_received(whisperr_message_id,foreground: true,source: foreground) once per message id (the last 50 are remembered across restarts). Only for Whisperr pushes.trackAutomaticEvents: falseturns it off. -
Versions in every event context. Every event (custom and automatic) carries
app_version,app_build,platform,sdk_nameandsdk_versionincontext. No value, no key. A key the caller passes incontextwins. The SDK never putslocalein context.client_app_idis never an event property. -
Automatic events (also
push_permission_changed) always carryplatformandos_name, also when the app context cannot be read. -
Calls before
start().setPushTokenandsetAppLanguagecalled beforestart()are applied after the stored state is loaded. Identity calls made whilestart()loads the stored state wait for it. -
Conformance: the suites run whisperr-spec
v2.json(restoreIdentify, appLanguage, anonymousPush, pushRegistration, versionContext) andautomatic.jsonpushEventsV2, and skip the legacy cases they supersede. The v2 harness compares identify bodies and events as two ordered lists. -
Removed: the identify trait
push_permission.setPushPermission()no longer writes it. An app or a dashboard filter that reads this trait gets no new values. Use the eventpush_permission_changed(below) instead. -
Push permission and the token (whisperr-spec SPEC.md "Push permission and the token"):
deniedopts this device's push token out and holds it. A token set whiledeniedis held, not sent.grantedorprovisionalopts the held token back in. When this token was the user's only opted-in channel, the server marks the user suppressed (all_channels_opted_out) until a channel is opted in again. -
setPushPermissionsends the eventpush_permission_changed. The event hasstatus(authorized,provisional,denied,not_determined) andprevious_statuswhen the status changed. Before, the SDK sent the identify traitpush_permissionwith other values (granted,undetermined), and the engine did not read it. The SDK no longer sends the trait.WhisperrPushPermissionkeeps its values.grantedgoes out asauthorizedandundeterminedasnot_determined.WhisperrPushPermission.wireValuenow returns these event values.- The SDK stores the last status it sent on this device and sends a status
only when it changed.
reset()forgets it. Before login, the event goes out under theanonymous_id. The event goes out also when automatic events are off. - After an upgrade from 0.5.x, the first report sends the event once, with
no
previous_status. deniedstill opts out this device's push token and holds it. The identify for this now carries onlyexternal_user_idandchannels.
-
optOut()andoptIn().optOut()now tells the server about this device. When the SDK registered a push token, it sends one identify that opts the token out, under the user the token was registered for. Push opt-outs already queued (a rotation, a denied permission, an earlieroptOut()) stay queued ahead of it. The SDK delivers and retries these requests also while opted out and after a restart.optOut()forgets the last-sent token, so afteroptIn()the nextsetPushTokenregisters it again.- An install that a 0.5.x SDK opted out still holds the last-sent token. At start, the SDK sends the same opt-out once and forgets the token.
-
Deprecated:
setOptOut(bool). It callsoptOut()oroptIn(). -
Tests run every case of whisperr-spec
conformance/automatic.json.
0.5.0 #
- Push token kinds (whisperr-spec
push.jsonkindCases):setPushToken(token, kind:, platform:, pushEnv:)andattachPushTokenStream(stream, kind:, …). New enumsWhisperrPushTokenKindandWhisperrPushEnvironment. With any metadata,platformdefaults to the device OS and an Expo token getskind: expo;pushEnvis never guessed. A bare token is unchanged on the wire. A token re-sent with new metadata goes out once more; a bare token never removes metadata already sent.WhisperrChannel.pushtakes the same fields. setPushPermission(WhisperrPushPermission): sends the traitpush_permission, deduped across restarts.deniedopts out this device's token and holds it until the permission comes back. Before login, the status goes with the nextidentify().- Deep links:
trackPushOpenedreadswhisperr_deep_link, thendeep_link. NewWhisperrPushOpen.fromData(data)returns the message id and deep link for routing. - New package
whisperr_firebase_messaging:registerFirebaseMessaging,handleRemoteMessage,handleNotificationOpens.
0.4.0 #
This is a minor release because automatic events are on by default. Apps
pinned to ^0.3.x do not get them until they move to ^0.4.0.
- Automatic app events, on by default. The SDK sends
app_installed,app_updated,app_openedandapp_backgrounded. Each SDK event carries the SDK name and version, the app version and build, the platform, the OS, the locale and the time zone. Newscreen(name)sendsscreen_viewed. - Off switch. Set
WhisperrOptions(trackAutomaticEvents: false)to turn automatic events off. - Events before identify (anonymous lane).
track()no longer throws when there is no user. It sends the event under a savedanonymous_id. The nextidentify()carries the same id, so the server merges those events into the user.reset()makes a new id. - Push opens. New
trackPushOpened(data)sendspush_openedfor Whisperr pushes. It sends each message only once, also after a restart. - Opt-out. New
setOptOut(bool)andisOptedOut. Opt-out deletes the queue and stops all sending. The SDK saves the choice. - Retry-After. On
429and503, the SDK waits for the time inRetry-After(at most 60 s). The retry limit does not change. - Email is not marked verified by default. The
email:shortcut onidentify()sends noverifiedfield. The server decides. To set it, buildchannelsyourself. - The SDK also flushes when the app becomes hidden.
- New dependency:
package_info_plus(>=8.0.0 <11.0.0). The SDK uses it to read the app version and build. - Minimum Flutter is now 3.19.
0.3.5 #
- Add
identify(requirePersistence: true)to reject channel transitions when local queue storage fails or persistence is disabled. - Protect identify operations from telemetry overflow. A queue containing only identifies rejects additional identifies instead of losing pending changes.
- Serialize queue updates and persistence writes, including delivery acknowledgments, to prevent stale storage snapshots.
- Treat unsuccessful SharedPreferences writes as storage errors.
0.3.4 #
- Fix event loss when the bounded queue overflows during an in-flight request. Identify and batch acknowledgments now remove only the original operation IDs. Permanent errors cannot drop newer queued work or clear a replacement push registration's deduplication mark after the original request was evicted.
0.3.3 #
- Add optional
reset(flushBeforeReset: false)to clear local identity without waiting for network delivery during logout. Default reset behavior is unchanged.
0.3.2 #
identify()now fills the reserved traitslocale(BCP 47, from the platform locale) andtimezone_offset_minutes(the device's UTC offset in minutes, east-positive) by default, so the engine can pick the message language and approximate quiet hours. Flutter cannot obtain an IANA zone name without a plugin, sotimezoneis never guessed — passtraits: {'timezone': 'Europe/Berlin'}when the app knows it, and the offset fallback is dropped. Caller-supplied values always win;setPushToken()'s partial identify stays traits-free.WhisperrClientgains an injectabledeviceTraitsresolver for tests.
0.3.1 #
- Fix:
attachPushTokenStreamnow guards against uncaught async errors. The listener has anonErrorhandler (a failing token source is reported, not thrown into the zone) and wrapssetPushTokenso its rejections can't escape and crash the app.close()now cancels every subscription opened byattachPushTokenStream, so a token emitted after teardown can't reach a dead client. - Fix:
setPushToken('')/ whitespace-only tokens are now silently ignored instead of throwingArgumentError—getToken()can return an empty string before the device registers, and the method is documented as safe to call on every launch. This aligns Flutter with the React Native and Swift SDKs. - Fix: the dedup pair is a mark of what was delivered. A registration whose
request is dropped (non-retryable
4xx) or evicted on queue overflow now clears the pair, so the token re-registers next time instead of being wedged opted-out forever by a single rejection. identify(pushToken:)(or an explicit push channel on identify) now rotates likesetPushToken: a differing token opts the previous one out in the same body instead of stranding it opted-in.- Verified against the hardened
whisperr-specconformance/push.json(reset, empty-token,identify(pushToken:), and restart-then-reidentify cases).
0.3.0 #
setPushToken(token): first-class push-token capture. Re-identifies thepushchannel for the current user, buffers tokens set beforeidentify(), no-ops on repeated tokens, and opts the previous token out on rotation — matching the other Whisperr SDKs and verified against the newwhisperr-specconformance/push.jsonfixtures.- The last-sent (user, push token) pair and the identified user are persisted and restored on start, per the spec: setting the same token again is a no-op even after an app restart, and a token rotation after a relaunch still opts the stale token out.
attachPushTokenStream(tokens): dependency-free glue forFirebaseMessaging.instance.onTokenRefresh(or any token stream).reset()now also clears buffered/remembered push tokens, including the persisted last-sent pair and identity.- Breaking (custom persistence only):
WhisperrPersistenceis now slot-based —load/save/cleartake a slot name (queue,identity,push). The defaultSharedPreferencesPersistencekeeps the existingwhisperr.queue.v1key and addswhisperr.identity.v1/whisperr.push.v1; persisted queues from older versions are unaffected.
0.2.4 #
- Truncate
occurred_atto millisecond precision (RFC3339Z), matching the spec and the other Whisperr SDKs. Dart'sDateTime.toIso8601String()emits microseconds, which previously went out verbatim. - Validate
event_typeclient-side: invalid names are dropped (surfaced viaonError) instead of being sent, so one malformed event can't make the server reject an entire batch.
0.2.3 #
- Sync the reported SDK version (
kWhisperrSdkVersion) with the package version; it had drifted to0.2.0.
0.2.2 #
- Wire-format conformance with the other Whisperr SDKs (verified against
whisperr-spec):identify()now supportspreferred_channel, shortcut channels send an explicitopted_in, and emptytrack()events include a defaultpropertiesobject.
0.2.1 #
- Each
track()event now carries a stable per-event idempotency key ($message_id) incontext, reusing the persisted queue op id so it survives restarts and retries. Prevents duplicate events when the durable queue resends after a timeout, matching server-side dedup.
0.2.0 #
- Breaking:
identify()no longer acceptspreferredChannel. Whisperr now derives the best channel from engagement; express an explicit user choice withoptedIn: falseon the channels they don't want. - Added
email,phone, andpushTokenshortcut parameters toidentify()that expand into opted-in channels — no need to buildWhisperrChannelobjects for the common case.
0.1.0 #
- Initial release.
Whisperr.initialize,identify,track,flush,reset.- Durable, ordered outbound queue with batched delivery (
/v1/events/batch), offline persistence, exponential-backoff retry, and 429/auth/client-error handling. - App-lifecycle flush on pause/detach.