Identify traits are not persisted across cold starts - parity gap vs analytics-kotlin / analytics-android

Open
#209 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
50/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet
Tech stack
dart, flutter
Domain
analytics, mobile

Research direction

Start at the Flutter SDK's createClient/init, identify, and reset paths, then compare their trait handling with analytics-kotlin's Traits.kt and UserInfo persistence via Storage.Constants.Traits. Done means traits survive cold starts, successive identify calls deep-merge and persist them, and reset clears the persisted map; verify against the reproduction payload.

Written by the indexing model from the issue text.

Description

Package

segment_analytics: ^1.1.11

Summary

On every fresh app launch, the Flutter SDK starts with an empty trait
set. Traits sent to analytics.identify(userId, userTraits) in a previous
session are not read back from disk on init, and subsequent identify(...)
calls do NOT merge with the previously-shipped traits — the SDK ships only
what the caller passed in that single call.

This diverges from the Segment analytics-kotlin / analytics-android
SDKs, which persist the last-known Traits on disk and merge incoming
traits into that persisted map on every identify call. As a result, the
event that lands on the Segment ingestion endpoint from Android carries the
full accumulated trait union; the same app port to Flutter carries only the
delta from the most recent call.

Reproduction
  1. Fresh install, cold start.
  2. analytics.identify('u1', UserTraits(custom: {'a': 1}))
  3. Kill the process. Cold start again.
  4. analytics.identify('u1', UserTraits(custom: {'b': 2}))
  5. Inspect the outbound identify payload — traits contains only {b: 2}.
    Expected (Android parity): {a: 1, b: 2}.

Same divergence is observable within a single session across multiple
identify calls: call 2's payload does not include the traits shipped in
call 1.

Expected behavior

Match analytics-kotlin (see Traits.kt + UserInfo persistence via
Storage.write(Storage.Constants.Traits, ...)):

  • Read persisted traits from disk on createClient / init.
  • On every identify, deep-merge incoming traits into the persisted map
    and write back.
  • On reset(), clear the persisted map.

Every app that ports from analytics-android/kotlin has to reimplement this
just to keep dashboards keyed on cumulative traits working. Would be great
to have it in the SDK itself.

Questions
  1. Is trait persistence on the roadmap, or is the current wholesale-replace
    behavior intentional?
  2. If intentional, what's the recommended pattern for apps that need
    analytics-kotlin-parity accumulation? (Is a Plugin of type
    enrichment on the identify event the right hook, or should apps keep
    doing the merge above the SDK boundary as we do today?)

###Environment

  • segment_analytics: 1.1.11
  • Flutter: 3.11.0
  • Platform: Android + iOS (same behavior on both)
Current workaround

We accumulate traits manually in the app layer:

  // Full trait union re-sent on every identify.                                                                                                                                     
  final Map<String, Object?> _accumulatedTraits = <String, Object?>{};                                                                                                               
                                                                                                                                                                                     
  Future<void> init() async {                                                                                                                                                        
    _hydrateAccumulatedTraits(); // reads jsonEncoded map from SharedPreferences                                                                                                     
    ...                                                                                                                                                                              
  }                                                                                                                                                                                  
                                                                                                                                                                                     
  Future<void> identify({String? userId, required Map<String, Object?> traits}) async {                                                                                              
    _accumulatedTraits.addAll(traits);                                                                                                                                               
    unawaited(_prefs.setAccumulatedTraits(jsonEncode(_accumulatedTraits)));                                                                                                          
    await _segment.identify(                                                                                                                                                         
      userId: userId,                                                                                                                                                                
      userTraits: UserTraits(custom: Map<String, dynamic>.from(_accumulatedTraits)),                                                                                                 
    );                                                                                                                                                                               
  }                                                                                                                                                                                  
                                                                                                                                                                                     
  Future<void> reset() async {                                                                                                                                                       
    _accumulatedTraits.clear();                                                                                                                                                      
    unawaited(_prefs.setAccumulatedTraits(null));                                                                                                                                    
    return _segment.reset(resetAnonymousId: true);                                                                                                                                   
  }    
Dominant language
Dart
Stars
33
Forks
57
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from segmentio/analytics_flutter

All issues in segmentio/analytics_flutter

Similar issues

More Dart issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.