Skip to content

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

Description

@prudhvi-kumar-android

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);                                                                                                                                   
  }    

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions