@@ -136,8 +136,8 @@ func TestCacheAnalyticsCollector_FieldHashing(t *testing.T) {
136136 t .Run ("same input produces same hash" , func (t * testing.T ) {
137137 c := NewCacheAnalyticsCollector ()
138138
139- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph )
140- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph )
139+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph , "" )
140+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph , "" )
141141
142142 snap := c .Snapshot ()
143143 assert .Equal (t , 2 , len (snap .FieldHashes ))
@@ -151,8 +151,8 @@ func TestCacheAnalyticsCollector_FieldHashing(t *testing.T) {
151151 t .Run ("different input produces different hash" , func (t * testing.T ) {
152152 c := NewCacheAnalyticsCollector ()
153153
154- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph )
155- c .HashFieldValue ("User" , "name" , []byte (`"Bob"` ), `{"id":"2"}` , 0 , FieldSourceSubgraph )
154+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph , "" )
155+ c .HashFieldValue ("User" , "name" , []byte (`"Bob"` ), `{"id":"2"}` , 0 , FieldSourceSubgraph , "" )
156156
157157 snap := c .Snapshot ()
158158 assert .Equal (t , 2 , len (snap .FieldHashes ))
@@ -162,7 +162,7 @@ func TestCacheAnalyticsCollector_FieldHashing(t *testing.T) {
162162 t .Run ("hashed keys mode" , func (t * testing.T ) {
163163 c := NewCacheAnalyticsCollector ()
164164
165- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), "" , 12345 , FieldSourceL1 )
165+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), "" , 12345 , FieldSourceL1 , "" )
166166
167167 snap := c .Snapshot ()
168168 assert .Equal (t , 1 , len (snap .FieldHashes ))
@@ -174,9 +174,9 @@ func TestCacheAnalyticsCollector_FieldHashing(t *testing.T) {
174174 t .Run ("field source tracking" , func (t * testing.T ) {
175175 c := NewCacheAnalyticsCollector ()
176176
177- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph )
178- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceL1 )
179- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceL2 )
177+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph , "" )
178+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceL1 , "" )
179+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceL2 , "" )
180180
181181 snap := c .Snapshot ()
182182 assert .Equal (t , 3 , len (snap .FieldHashes ))
@@ -234,9 +234,9 @@ func TestCacheAnalyticsCollector_EntityCounts(t *testing.T) {
234234func TestCacheAnalyticsCollector_EntitySourceTracking (t * testing.T ) {
235235 c := NewCacheAnalyticsCollector ()
236236
237- c .RecordEntitySource ("User" , `{"id":"1"}` , FieldSourceSubgraph )
238- c .RecordEntitySource ("User" , `{"id":"2"}` , FieldSourceL1 )
239- c .RecordEntitySource ("Product" , `{"upc":"top-1"}` , FieldSourceL2 )
237+ c .RecordEntitySource ("User" , `{"id":"1"}` , "" , FieldSourceSubgraph )
238+ c .RecordEntitySource ("User" , `{"id":"2"}` , "" , FieldSourceL1 )
239+ c .RecordEntitySource ("Product" , `{"upc":"top-1"}` , "" , FieldSourceL2 )
240240
241241 assert .Equal (t , FieldSourceSubgraph , c .EntitySource ("User" , `{"id":"1"}` ))
242242 assert .Equal (t , FieldSourceL1 , c .EntitySource ("User" , `{"id":"2"}` ))
@@ -1564,7 +1564,7 @@ func TestFieldSourceShadowCached(t *testing.T) {
15641564 t .Run ("HashFieldValue with FieldSourceShadowCached" , func (t * testing.T ) {
15651565 c := NewCacheAnalyticsCollector ()
15661566
1567- c .HashFieldValue ("User" , "username" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceShadowCached )
1567+ c .HashFieldValue ("User" , "username" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceShadowCached , "" )
15681568
15691569 snap := c .Snapshot ()
15701570 require .Equal (t , 1 , len (snap .FieldHashes ))
@@ -1577,8 +1577,8 @@ func TestFieldSourceShadowCached(t *testing.T) {
15771577 t .Run ("can distinguish from other sources" , func (t * testing.T ) {
15781578 c := NewCacheAnalyticsCollector ()
15791579
1580- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph )
1581- c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceShadowCached )
1580+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceSubgraph , "" )
1581+ c .HashFieldValue ("User" , "name" , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceShadowCached , "" )
15821582
15831583 snap := c .Snapshot ()
15841584 require .Equal (t , 2 , len (snap .FieldHashes ))
@@ -1755,7 +1755,7 @@ func BenchmarkFieldHashing(b *testing.B) {
17551755
17561756 b .ResetTimer ()
17571757 for b .Loop () {
1758- c .HashFieldValue ("User" , "id" , value , `{"id":"1"}` , 0 , FieldSourceSubgraph )
1758+ c .HashFieldValue ("User" , "id" , value , `{"id":"1"}` , 0 , FieldSourceSubgraph , "" )
17591759 }
17601760}
17611761
@@ -2056,7 +2056,7 @@ func TestSnapshotSlicesAreIndependent(t *testing.T) {
20562056 c .RecordError (SubgraphErrorEvent {DataSource : "ds-orig" })
20572057 c .RecordMutationEvent (MutationEvent {EntityType : "User-orig" })
20582058 c .RecordCacheOperationError (CacheOperationError {DataSource : "ds-orig" })
2059- c .HashFieldValue ("User-orig" , "name" , []byte (`"a"` ), "k-orig" , 1 , FieldSourceL1 )
2059+ c .HashFieldValue ("User-orig" , "name" , []byte (`"a"` ), "k-orig" , 1 , FieldSourceL1 , "" )
20602060
20612061 snap := c .Snapshot ()
20622062
@@ -2078,7 +2078,7 @@ func TestSnapshotSlicesAreIndependent(t *testing.T) {
20782078 c .RecordError (SubgraphErrorEvent {DataSource : "ds-new" })
20792079 c .RecordMutationEvent (MutationEvent {EntityType : "User-new" })
20802080 c .RecordCacheOperationError (CacheOperationError {DataSource : "ds-new" })
2081- c .HashFieldValue ("User-new" , "name" , []byte (`"z"` ), "k-new" , 2 , FieldSourceL2 )
2081+ c .HashFieldValue ("User-new" , "name" , []byte (`"z"` ), "k-new" , 2 , FieldSourceL2 , "" )
20822082 }
20832083
20842084 // Full-slice assertions — snapshot must still show the original events.
@@ -2175,3 +2175,88 @@ func TestCacheAnalyticsCollector_TimestampsPreservePreSet(t *testing.T) {
21752175 }
21762176}
21772177
2178+ // TestCacheAnalyticsCollector_DataSourceOnHashFieldValue verifies that the
2179+ // dataSource argument is recorded on EntityFieldHash. Without this, dashboards
2180+ // cannot attribute a field hash to the subgraph that produced the data when
2181+ // multiple subgraphs contribute fields to the same entity.
2182+ func TestCacheAnalyticsCollector_DataSourceOnHashFieldValue (t * testing.T ) {
2183+ c := NewCacheAnalyticsCollector ()
2184+
2185+ // Same entity (User id=1) resolved by two subgraphs — the field-hash
2186+ // records must distinguish which subgraph each value came from.
2187+ c .HashFieldValue ("User" , "name" , nil , []byte (`"Alice"` ), `{"id":"1"}` , 0 , FieldSourceL2 , "accounts" )
2188+ c .HashFieldValue ("User" , "reviews" , nil , []byte (`[]` ), `{"id":"1"}` , 0 , FieldSourceSubgraph , "reviews" )
2189+
2190+ snap := c .Snapshot ()
2191+ require .Equal (t , 2 , len (snap .FieldHashes ))
2192+ assert .Equal (t , "accounts" , snap .FieldHashes [0 ].DataSource , "first hash attributes to accounts subgraph" )
2193+ assert .Equal (t , "reviews" , snap .FieldHashes [1 ].DataSource , "second hash attributes to reviews subgraph" )
2194+ }
2195+
2196+ // TestCacheAnalyticsCollector_EntityDataSource verifies the EntityDataSource
2197+ // reverse lookup returns the recorded subgraph and "" for unknown entities.
2198+ // Last-write-wins on duplicate keys, mirroring EntitySource semantics.
2199+ func TestCacheAnalyticsCollector_EntityDataSource (t * testing.T ) {
2200+ c := NewCacheAnalyticsCollector ()
2201+
2202+ // Three distinct entities owned by three subgraphs.
2203+ c .RecordEntitySource ("User" , `{"id":"1"}` , "accounts" , FieldSourceSubgraph )
2204+ c .RecordEntitySource ("Product" , `{"upc":"top-1"}` , "products" , FieldSourceL2 )
2205+ c .RecordEntitySource ("Review" , `{"id":"r1"}` , "reviews" , FieldSourceL1 )
2206+
2207+ assert .Equal (t , "accounts" , c .EntityDataSource ("User" , `{"id":"1"}` ))
2208+ assert .Equal (t , "products" , c .EntityDataSource ("Product" , `{"upc":"top-1"}` ))
2209+ assert .Equal (t , "reviews" , c .EntityDataSource ("Review" , `{"id":"r1"}` ))
2210+ assert .Equal (t , "" , c .EntityDataSource ("Unknown" , `{"id":"99"}` ), "unknown entity returns empty string" )
2211+
2212+ // Last-write-wins: a second record for the same (type, key) overrides.
2213+ c .RecordEntitySource ("User" , `{"id":"1"}` , "accounts-v2" , FieldSourceL1 )
2214+ assert .Equal (t , "accounts-v2" , c .EntityDataSource ("User" , `{"id":"1"}` ), "most recent record wins" )
2215+ }
2216+
2217+ // TestCacheAnalyticsCollector_MutationEventDataSource verifies that DataSource
2218+ // is preserved through RecordMutationEvent. Mutation analytics need to know
2219+ // which subgraph owns the mutated entity for cross-subgraph dashboards.
2220+ func TestCacheAnalyticsCollector_MutationEventDataSource (t * testing.T ) {
2221+ c := NewCacheAnalyticsCollector ()
2222+
2223+ c .RecordMutationEvent (MutationEvent {
2224+ MutationRootField : "updateUsername" ,
2225+ EntityType : "User" ,
2226+ DataSource : "accounts" ,
2227+ EntityCacheKey : `{"__typename":"User","key":{"id":"1"}}` ,
2228+ HadCachedValue : true ,
2229+ IsStale : true ,
2230+ })
2231+
2232+ snap := c .Snapshot ()
2233+ require .Equal (t , 1 , len (snap .MutationEvents ))
2234+ assert .Equal (t , "accounts" , snap .MutationEvents [0 ].DataSource )
2235+ }
2236+
2237+ // TestWalkCachedResponseForSources_DataSource verifies that
2238+ // walkCachedResponseForSources propagates the dataSource argument to every
2239+ // entity record it produces while traversing a cached JSON value. Without
2240+ // this propagation, L2 cache hits would land in entitySources without
2241+ // subgraph attribution and the resolvable would have to fall back to
2242+ // obj.SourceName for every entity rendered from cache.
2243+ func TestWalkCachedResponseForSources_DataSource (t * testing.T ) {
2244+ a := arena .NewMonotonicArena (arena .WithMinBufferSize (1024 ))
2245+ parser := & astjson.Parser {}
2246+
2247+ value , err := parser .ParseBytesWithArena (a , []byte (`[{"id":"1","name":"Alice"},{"id":"2","name":"Bob"}]` ))
2248+ require .NoError (t , err )
2249+
2250+ keyFields := []KeyField {{Name : "id" }}
2251+ var sources []entitySourceRecord
2252+
2253+ walkCachedResponseForSources (value , keyFields , "User" , "accounts" , FieldSourceL2 , & sources )
2254+
2255+ require .Equal (t , 2 , len (sources ))
2256+ for i , want := range []string {`{"id":"1"}` , `{"id":"2"}` } {
2257+ assert .Equal (t , "User" , sources [i ].entityType )
2258+ assert .Equal (t , want , sources [i ].keyJSON )
2259+ assert .Equal (t , FieldSourceL2 , sources [i ].source )
2260+ assert .Equal (t , "accounts" , sources [i ].dataSource , "every entity record must carry the dataSource" )
2261+ }
2262+ }
0 commit comments