Skip to content

Commit d7013c7

Browse files
authored
add session replay to gcir (#304)
kong statsig-io/kong#3800 <img width="527" height="320" alt="Screenshot 2025-09-12 at 3 03 47 PM" src="https://github.com/user-attachments/assets/08c10fc9-2374-49ab-8f1d-2d0b579c3b5f" />
1 parent ac65a87 commit d7013c7

File tree

5 files changed

+111
-22
lines changed

5 files changed

+111
-22
lines changed

client_initialize_response.go

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
package statsig
22

33
import (
4+
"math/rand"
45
"strings"
56
)
67

8+
type GCIRSessionReplayTrigger struct {
9+
Values *[]string `json:"values,omitempty"`
10+
PassesSampling *bool `json:"passes_sampling,omitempty"`
11+
}
12+
713
type ClientInitializeResponse struct {
8-
FeatureGates map[string]GateInitializeResponse `json:"feature_gates"`
9-
DynamicConfigs map[string]ConfigInitializeResponse `json:"dynamic_configs"`
10-
LayerConfigs map[string]LayerInitializeResponse `json:"layer_configs"`
11-
SdkParams map[string]string `json:"sdkParams"`
12-
HasUpdates bool `json:"has_updates"`
13-
Generator string `json:"generator"`
14-
EvaluatedKeys map[string]interface{} `json:"evaluated_keys"`
15-
Time int64 `json:"time"`
16-
SDKInfo SDKInfo `json:"sdkInfo"`
17-
User User `json:"user"`
18-
HashUsed string `json:"hash_used"`
14+
FeatureGates map[string]GateInitializeResponse `json:"feature_gates"`
15+
DynamicConfigs map[string]ConfigInitializeResponse `json:"dynamic_configs"`
16+
LayerConfigs map[string]LayerInitializeResponse `json:"layer_configs"`
17+
SdkParams map[string]string `json:"sdkParams"`
18+
HasUpdates bool `json:"has_updates"`
19+
Generator string `json:"generator"`
20+
EvaluatedKeys map[string]interface{} `json:"evaluated_keys"`
21+
Time int64 `json:"time"`
22+
SDKInfo SDKInfo `json:"sdkInfo"`
23+
User User `json:"user"`
24+
HashUsed string `json:"hash_used"`
25+
CanRecordSession *bool `json:"can_record_session,omitempty"`
26+
SessionRecordingRate *float64 `json:"session_recording_rate,omitempty"`
27+
RecordingBlocked *bool `json:"recording_blocked,omitempty"`
28+
PassesSessionRecordingTargeting *bool `json:"passes_session_recording_targeting,omitempty"`
29+
SessionRecordingEventTriggers *map[string]GCIRSessionReplayTrigger `json:"session_recording_event_triggers,omitempty"`
30+
SessionRecordingExposureTriggers *map[string]GCIRSessionReplayTrigger `json:"session_recording_exposure_triggers,omitempty"`
1931
}
2032

2133
type SDKInfo struct {
@@ -148,7 +160,6 @@ func getClientInitializeResponse(
148160
}
149161
return hashedName, result
150162
}
151-
152163
cmabToResponse := func(cmabName string, spec configSpec) (string, ConfigInitializeResponse) {
153164
evalRes := &evalResult{}
154165
if context.IncludeLocalOverrides || options.Overrides != nil {
@@ -163,11 +174,10 @@ func getClientInitializeResponse(
163174
hashedName, base := evalResultToBaseResponse(cmabName, evalRes)
164175
result := ConfigInitializeResponse{
165176
BaseSpecInitializeResponse: base,
166-
Value: evalRes.JsonValue,
177+
Value: evalRes.JsonValue,
167178
}
168179
return hashedName, result
169180
}
170-
171181
configToResponse := func(configName string, spec configSpec) (string, ConfigInitializeResponse) {
172182
evalRes := &evalResult{}
173183
hasExpOverride := false
@@ -385,5 +395,62 @@ func getClientInitializeResponse(
385395
User: *user.getCopyForLogging(),
386396
HashUsed: hashAlgorithm,
387397
}
398+
399+
sessionReplayInfo := e.store.getSessionReplayInfo()
400+
if sessionReplayInfo == nil {
401+
return response
402+
}
403+
404+
response.RecordingBlocked = sessionReplayInfo.RecordingBlocked
405+
canRecord := !*sessionReplayInfo.RecordingBlocked
406+
response.CanRecordSession = &canRecord
407+
if sessionReplayInfo.SamplingRate != nil {
408+
response.SessionRecordingRate = sessionReplayInfo.SamplingRate
409+
if rand.Float64() > *sessionReplayInfo.SamplingRate {
410+
canRecord = false
411+
response.CanRecordSession = &canRecord
412+
}
413+
}
414+
if sessionReplayInfo.TargetingGate != nil {
415+
res := response.FeatureGates[hashName(hashAlgorithm, *sessionReplayInfo.TargetingGate)]
416+
passesTargeting := res.Value
417+
if !passesTargeting {
418+
canRecord = false
419+
response.CanRecordSession = &canRecord
420+
}
421+
response.PassesSessionRecordingTargeting = &passesTargeting
422+
}
423+
if sessionReplayInfo.SessionRecordingEventTriggers != nil {
424+
resultEventTriggers := make(map[string]GCIRSessionReplayTrigger)
425+
for eventName, trigger := range *sessionReplayInfo.SessionRecordingEventTriggers {
426+
resultEventTrigger := GCIRSessionReplayTrigger{}
427+
if trigger.Values != nil {
428+
resultEventTrigger.Values = trigger.Values
429+
}
430+
if trigger.SamplingRate != nil {
431+
passesSampling := rand.Float64() <= *trigger.SamplingRate
432+
resultEventTrigger.PassesSampling = &passesSampling
433+
}
434+
resultEventTriggers[eventName] = resultEventTrigger
435+
}
436+
response.SessionRecordingEventTriggers = &resultEventTriggers
437+
}
438+
439+
if sessionReplayInfo.SessionRecordingExposureTriggers != nil {
440+
resultExposureTriggers := make(map[string]GCIRSessionReplayTrigger)
441+
for exposureName, trigger := range *sessionReplayInfo.SessionRecordingExposureTriggers {
442+
resultExposureTrigger := GCIRSessionReplayTrigger{}
443+
if trigger.Values != nil {
444+
resultExposureTrigger.Values = trigger.Values
445+
}
446+
if trigger.SamplingRate != nil {
447+
passesSampling := rand.Float64() <= *trigger.SamplingRate
448+
resultExposureTrigger.PassesSampling = &passesSampling
449+
}
450+
resultExposureTriggers[hashName(hashAlgorithm, exposureName)] = resultExposureTrigger
451+
}
452+
response.SessionRecordingExposureTriggers = &resultExposureTriggers
453+
}
454+
388455
return response
389456
}

output_logger.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const (
2020
)
2121

2222
var HIGH_CARDINALITY_TAGS = map[string]bool{
23-
"lcut": true,
23+
"lcut": true,
2424
"prev_lcut": true,
2525
}
2626

@@ -205,11 +205,11 @@ func (o *OutputLogger) LogConfigSyncUpdate(initialized bool, hasUpdate bool, lcu
205205
}
206206
lcutDiff := prevLcut - lcut
207207
absLcutDiff := intAbs(lcutDiff)
208-
o.Distribution("config_propagation_diff", float64(absLcutDiff), map[string]interface{}{
209-
"source": source,
210-
"source_api": api,
211-
"lcut": lcut,
212-
"prev_lcut": prevLcut,
208+
o.Distribution("config_propagation_diff", float64(absLcutDiff), map[string]interface{}{
209+
"source": source,
210+
"source_api": api,
211+
"lcut": lcut,
212+
"prev_lcut": prevLcut,
213213
})
214214
}
215215

statsig_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,4 @@ func (c *initContext) toInitDetails() InitializeDetails {
9797
SourceAPI: c.SourceAPI,
9898
StorePopulated: c.StorePopulated,
9999
}
100-
}
100+
}

store.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ type cmabGroupConfig struct {
6161
WeightsCategorical map[string]map[string]float64 `json:"weightsCategorical"`
6262
}
6363

64+
type SessionReplayTrigger struct {
65+
SamplingRate *float64 `json:"sampling_rate"`
66+
Values *[]string `json:"values"`
67+
}
68+
69+
type SessionReplayInfo struct {
70+
SamplingRate *float64 `json:"sampling_rate"`
71+
TargetingGate *string `json:"targeting_gate"`
72+
RecordingBlocked *bool `json:"recording_blocked"`
73+
SessionRecordingEventTriggers *map[string]SessionReplayTrigger `json:"session_recording_event_triggers"`
74+
SessionRecordingExposureTriggers *map[string]SessionReplayTrigger `json:"session_recording_exposure_triggers"`
75+
}
76+
6477
func (c configSpec) hasTargetAppID(appId string) bool {
6578
if appId == "" {
6679
return true
@@ -117,6 +130,7 @@ type downloadConfigSpecResponse struct {
117130
SDKFlags map[string]bool `json:"sdk_flags,omitempty"`
118131
SDKConfigs map[string]interface{} `json:"sdk_configs,omitempty"`
119132
AppID string `json:"app_id,omitempty"`
133+
SessionReplayInfo *SessionReplayInfo `json:"session_replay_info,omitempty"`
120134
}
121135

122136
type configEntities struct {
@@ -172,6 +186,7 @@ type store struct {
172186
sdkConfigs *SDKConfigs
173187
AppID string
174188
context *initContext
189+
sessionReplayInfo *SessionReplayInfo
175190
}
176191

177192
var syncOutdatedMax = 2 * time.Minute
@@ -360,6 +375,12 @@ func (s *store) getEntitiesForSDKKey(clientKey string) (configEntities, bool) {
360375
return entities, ok
361376
}
362377

378+
func (s *store) getSessionReplayInfo() *SessionReplayInfo {
379+
s.mu.RLock()
380+
defer s.mu.RUnlock()
381+
return s.sessionReplayInfo
382+
}
383+
363384
func (s *store) fetchConfigSpecsFromAdapter(context *initContext) {
364385
s.addDiagnostics().dataStoreConfigSpecs().fetch().start().mark()
365386
defer func() {
@@ -582,6 +603,7 @@ func (s *store) setConfigSpecs(specs downloadConfigSpecResponse) (bool, bool) {
582603
s.hashedSDKKeysToEntities = specs.HashedSDKKeysToEntities
583604
s.lastSyncTime = specs.Time
584605
s.AppID = specs.AppID
606+
s.sessionReplayInfo = specs.SessionReplayInfo
585607
s.mu.Unlock()
586608
return true, true
587609
}

util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,4 @@ func intAbs(a int64) int64 {
154154
return -a
155155
}
156156
return a
157-
}
157+
}

0 commit comments

Comments
 (0)