@adtrackify/at-service-common 4.0.5 → 4.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts +1 -0
  2. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +646 -0
  3. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +1 -0
  4. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts +1 -0
  5. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +327 -0
  6. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +1 -0
  7. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +250 -108
  8. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -1
  9. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js +37 -36
  10. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
  11. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.d.ts +1 -0
  12. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js +567 -0
  13. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -0
  14. package/dist/cjs/services/db/identity-cache-dynamodb-service/constants.d.ts +3 -0
  15. package/dist/cjs/services/db/identity-cache-dynamodb-service/constants.js +7 -0
  16. package/dist/cjs/services/db/identity-cache-dynamodb-service/constants.js.map +1 -0
  17. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.d.ts +14 -0
  18. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js +251 -0
  19. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js.map +1 -0
  20. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +24 -0
  21. package/dist/cjs/services/db/{identity-cache-dynamodb-service.js → identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js} +221 -343
  22. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -0
  23. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +22 -0
  24. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +375 -0
  25. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -0
  26. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.d.ts +4 -0
  27. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js +21 -0
  28. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js.map +1 -0
  29. package/dist/cjs/services/db/identity-cache-dynamodb-service/key-builders.d.ts +12 -0
  30. package/dist/cjs/services/db/identity-cache-dynamodb-service/key-builders.js +32 -0
  31. package/dist/cjs/services/db/identity-cache-dynamodb-service/key-builders.js.map +1 -0
  32. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.d.ts +205 -0
  33. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js +35 -0
  34. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js.map +1 -0
  35. package/dist/cjs/services/db/index.d.ts +4 -1
  36. package/dist/cjs/services/db/index.js +4 -1
  37. package/dist/cjs/services/db/index.js.map +1 -1
  38. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts +1 -0
  39. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +644 -0
  40. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +1 -0
  41. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts +1 -0
  42. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +325 -0
  43. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +1 -0
  44. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +250 -108
  45. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -1
  46. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js +37 -36
  47. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
  48. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.d.ts +1 -0
  49. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js +565 -0
  50. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -0
  51. package/dist/esm/services/db/identity-cache-dynamodb-service/constants.d.ts +3 -0
  52. package/dist/esm/services/db/identity-cache-dynamodb-service/constants.js +4 -0
  53. package/dist/esm/services/db/identity-cache-dynamodb-service/constants.js.map +1 -0
  54. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.d.ts +14 -0
  55. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js +234 -0
  56. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js.map +1 -0
  57. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +24 -0
  58. package/dist/esm/services/db/{identity-cache-dynamodb-service.js → identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js} +199 -321
  59. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -0
  60. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +22 -0
  61. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +368 -0
  62. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -0
  63. package/dist/esm/services/db/identity-cache-dynamodb-service/index.d.ts +4 -0
  64. package/dist/esm/services/db/identity-cache-dynamodb-service/index.js +5 -0
  65. package/dist/esm/services/db/identity-cache-dynamodb-service/index.js.map +1 -0
  66. package/dist/esm/services/db/identity-cache-dynamodb-service/key-builders.d.ts +12 -0
  67. package/dist/esm/services/db/identity-cache-dynamodb-service/key-builders.js +24 -0
  68. package/dist/esm/services/db/identity-cache-dynamodb-service/key-builders.js.map +1 -0
  69. package/dist/esm/services/db/identity-cache-dynamodb-service/types.d.ts +205 -0
  70. package/dist/esm/services/db/identity-cache-dynamodb-service/types.js +32 -0
  71. package/dist/esm/services/db/identity-cache-dynamodb-service/types.js.map +1 -0
  72. package/dist/esm/services/db/index.d.ts +4 -1
  73. package/dist/esm/services/db/index.js +4 -1
  74. package/dist/esm/services/db/index.js.map +1 -1
  75. package/package.json +2 -1
  76. package/dist/cjs/services/db/identity-cache-dynamodb-service.d.ts +0 -44
  77. package/dist/cjs/services/db/identity-cache-dynamodb-service.js.map +0 -1
  78. package/dist/esm/services/db/identity-cache-dynamodb-service.d.ts +0 -44
  79. package/dist/esm/services/db/identity-cache-dynamodb-service.js.map +0 -1
@@ -0,0 +1,646 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const at_tracking_event_types_1 = require("@adtrackify/at-tracking-event-types");
4
+ jest.mock('../../clients/index.js', () => ({
5
+ DynamoDbClient: {
6
+ safeGet: jest.fn(),
7
+ safePut: jest.fn(),
8
+ safeDelete: jest.fn(),
9
+ safeBatchGet: jest.fn(),
10
+ safeBatchWrite: jest.fn(),
11
+ safeQueryByGSI: jest.fn(),
12
+ batchWrite: jest.fn(),
13
+ },
14
+ }));
15
+ jest.mock('../../helpers/index.js', () => ({
16
+ Logger: {
17
+ debug: jest.fn(),
18
+ info: jest.fn(),
19
+ warn: jest.fn(),
20
+ error: jest.fn(),
21
+ },
22
+ }));
23
+ const index_js_1 = require("../../clients/index.js");
24
+ const index_js_2 = require("../../helpers/index.js");
25
+ const identity_cache_dynamodb_db_service_js_1 = require("../../services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js");
26
+ const types_js_1 = require("../../services/db/identity-cache-dynamodb-service/types.js");
27
+ const mockedDynamoDbClient = index_js_1.DynamoDbClient;
28
+ const mockedLogger = index_js_2.Logger;
29
+ const pixelId = 'pixel-cache-lookup-v2';
30
+ let dateNowSpy;
31
+ const deepClone = (value) => {
32
+ if (value === undefined) {
33
+ return value;
34
+ }
35
+ return JSON.parse(JSON.stringify(value));
36
+ };
37
+ const normalizeDynamicValues = (value) => {
38
+ if (Array.isArray(value)) {
39
+ return value.map(normalizeDynamicValues);
40
+ }
41
+ if (value && typeof value === 'object') {
42
+ const normalized = {};
43
+ for (const [key, objectValue] of Object.entries(value)) {
44
+ if (key === 'updatedAt' || key === 'createdAt') {
45
+ normalized[key] = '<timestamp>';
46
+ }
47
+ else {
48
+ normalized[key] = normalizeDynamicValues(objectValue);
49
+ }
50
+ }
51
+ return normalized;
52
+ }
53
+ return value;
54
+ };
55
+ const cloneCalls = (mockFunction) => normalizeDynamicValues(deepClone(mockFunction.mock.calls));
56
+ const captureDynamoDbCalls = () => ({
57
+ safeGet: cloneCalls(mockedDynamoDbClient.safeGet),
58
+ safePut: cloneCalls(mockedDynamoDbClient.safePut),
59
+ safeDelete: cloneCalls(mockedDynamoDbClient.safeDelete),
60
+ safeBatchGet: cloneCalls(mockedDynamoDbClient.safeBatchGet),
61
+ safeBatchWrite: cloneCalls(mockedDynamoDbClient.safeBatchWrite),
62
+ safeQueryByGSI: cloneCalls(mockedDynamoDbClient.safeQueryByGSI),
63
+ batchWrite: cloneCalls(mockedDynamoDbClient.batchWrite),
64
+ });
65
+ const captureLoggerCalls = () => ({
66
+ debug: cloneCalls(mockedLogger.debug),
67
+ info: cloneCalls(mockedLogger.info),
68
+ warn: cloneCalls(mockedLogger.warn),
69
+ error: cloneCalls(mockedLogger.error),
70
+ });
71
+ const resetMocksForRun = () => {
72
+ dateNowSpy?.mockRestore();
73
+ dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(1782532911813);
74
+ jest.clearAllMocks();
75
+ mockedDynamoDbClient.safeGet.mockReset();
76
+ mockedDynamoDbClient.safePut.mockReset();
77
+ mockedDynamoDbClient.safeDelete.mockReset();
78
+ mockedDynamoDbClient.safeBatchGet.mockReset();
79
+ mockedDynamoDbClient.safeBatchWrite.mockReset();
80
+ mockedDynamoDbClient.safeQueryByGSI.mockReset();
81
+ mockedDynamoDbClient.batchWrite.mockReset();
82
+ };
83
+ const buildCachedRecord = (identity) => ({
84
+ pk: `identity#${pixelId}#${identity.identityId}`,
85
+ pixelId,
86
+ identityId: identity.identityId ?? 'missing-id',
87
+ response: deepClone(identity),
88
+ updatedAt: '2026-01-01T00:00:00.000Z',
89
+ });
90
+ const runOriginal = async (scenario) => {
91
+ resetMocksForRun();
92
+ scenario.setupMocks();
93
+ const cacheResult = await identity_cache_dynamodb_db_service_js_1.IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, deepClone(scenario.incomingIdentity));
94
+ return {
95
+ cacheResult: normalizeDynamicValues(deepClone(cacheResult)),
96
+ dynamoDbCalls: captureDynamoDbCalls(),
97
+ loggerCalls: captureLoggerCalls(),
98
+ };
99
+ };
100
+ const runV2 = async (scenario) => {
101
+ resetMocksForRun();
102
+ scenario.setupMocks();
103
+ const result = await identity_cache_dynamodb_db_service_js_1.IdentityCacheDynamoDbDbService.getIdentityFromCacheV2(pixelId, deepClone(scenario.incomingIdentity));
104
+ return {
105
+ cacheResult: normalizeDynamicValues(deepClone(result.cacheResult)),
106
+ summary: normalizeDynamicValues(deepClone(result.summary)),
107
+ dynamoDbCalls: captureDynamoDbCalls(),
108
+ loggerCalls: captureLoggerCalls(),
109
+ };
110
+ };
111
+ const expectV2ToMatchOriginal = async (scenario) => {
112
+ const originalRun = await runOriginal(scenario);
113
+ const v2Run = await runV2(scenario);
114
+ expect(v2Run.cacheResult).toEqual(originalRun.cacheResult);
115
+ expect(v2Run.dynamoDbCalls).toEqual(originalRun.dynamoDbCalls);
116
+ expect(v2Run.loggerCalls).toEqual(originalRun.loggerCalls);
117
+ expect(v2Run.summary.outcome).toBe(scenario.expectedOutcome);
118
+ scenario.assertSummary?.(v2Run.summary);
119
+ return { originalRun, v2Run };
120
+ };
121
+ describe('IdentityCacheDynamoDbDbService getIdentityFromCache() vs getIdentityFromCacheV2()', () => {
122
+ it('matches invalid input behavior', async () => {
123
+ await expectV2ToMatchOriginal({
124
+ incomingIdentity: null,
125
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.INVALID_INPUT,
126
+ setupMocks: () => undefined,
127
+ assertSummary: (summary) => {
128
+ expect(summary.isCacheStale).toBe(true);
129
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
130
+ },
131
+ });
132
+ });
133
+ it('matches primary identity cache miss behavior', async () => {
134
+ await expectV2ToMatchOriginal({
135
+ incomingIdentity: {
136
+ identityId: 'missing-primary',
137
+ traits: { emails: ['missing@example.com'] },
138
+ },
139
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_MISS,
140
+ setupMocks: () => {
141
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
142
+ },
143
+ assertSummary: (summary) => {
144
+ expect(summary.didLookupByIdentityId).toBe(true);
145
+ expect(summary.didLookupByEmail).toBe(false);
146
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
147
+ },
148
+ });
149
+ });
150
+ it('matches cached identity without response behavior', async () => {
151
+ await expectV2ToMatchOriginal({
152
+ incomingIdentity: {
153
+ identityId: 'missing-response',
154
+ traits: { emails: ['missing-response@example.com'] },
155
+ },
156
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_MISS_NO_RESPONSE,
157
+ setupMocks: () => {
158
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
159
+ pk: `identity#${pixelId}#missing-response`,
160
+ pixelId,
161
+ identityId: 'missing-response',
162
+ updatedAt: '2026-01-01T00:00:00.000Z',
163
+ });
164
+ },
165
+ assertSummary: (summary) => {
166
+ expect(summary.didLookupByIdentityId).toBe(true);
167
+ expect(summary.isCacheStale).toBe(true);
168
+ },
169
+ });
170
+ });
171
+ it('matches primary cache hit behavior when traits are equal', async () => {
172
+ const cachedIdentity = {
173
+ identityId: 'fresh-primary',
174
+ traits: { emails: ['fresh@example.com'] },
175
+ };
176
+ await expectV2ToMatchOriginal({
177
+ incomingIdentity: deepClone(cachedIdentity),
178
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
179
+ setupMocks: () => {
180
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
181
+ },
182
+ assertSummary: (summary) => {
183
+ expect(summary.didLookupByIdentityId).toBe(true);
184
+ expect(summary.isDeepEqual).toBe(true);
185
+ expect(summary.isCacheStale).toBe(false);
186
+ },
187
+ });
188
+ });
189
+ it('matches primary stale behavior for identity traits', async () => {
190
+ const cachedIdentity = {
191
+ identityId: 'stale-primary',
192
+ traits: { emails: ['old@example.com'] },
193
+ };
194
+ await expectV2ToMatchOriginal({
195
+ incomingIdentity: {
196
+ identityId: 'stale-primary',
197
+ traits: { emails: ['old@example.com', 'new@example.com'] },
198
+ },
199
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_STALE,
200
+ setupMocks: () => {
201
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
202
+ },
203
+ assertSummary: (summary) => {
204
+ expect(summary.isCacheStale).toBe(true);
205
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.STALE_TRAITS);
206
+ expect(summary.skipNeptune).toBe(false);
207
+ expect(summary.staleTraits).toContain('emails');
208
+ },
209
+ });
210
+ });
211
+ it('matches primary address update behavior', async () => {
212
+ const cachedIdentity = {
213
+ identityId: 'address-primary',
214
+ traits: {
215
+ emails: ['address@example.com'],
216
+ addresses: [],
217
+ },
218
+ };
219
+ await expectV2ToMatchOriginal({
220
+ incomingIdentity: {
221
+ identityId: 'address-primary',
222
+ traits: {
223
+ emails: ['address@example.com'],
224
+ addresses: [{ city: 'Boston', s: at_tracking_event_types_1.ADDRESS_INFO_SOURCE.INPUT }],
225
+ },
226
+ },
227
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_STALE,
228
+ setupMocks: () => {
229
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
230
+ },
231
+ assertSummary: (summary) => {
232
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.ADDRESS_UPDATE);
233
+ expect(summary.skipNeptune).toBe(true);
234
+ expect(summary.staleTraits).toContain('addresses');
235
+ },
236
+ });
237
+ });
238
+ it('matches volatile-only behavior', async () => {
239
+ const cachedIdentity = {
240
+ identityId: 'volatile-primary',
241
+ traits: {
242
+ emails: ['volatile@example.com'],
243
+ ipAddress: '192.168.1.1',
244
+ },
245
+ };
246
+ await expectV2ToMatchOriginal({
247
+ incomingIdentity: {
248
+ identityId: 'volatile-primary',
249
+ traits: {
250
+ emails: ['volatile@example.com'],
251
+ ipAddress: '10.0.0.1',
252
+ },
253
+ },
254
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
255
+ setupMocks: () => {
256
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
257
+ },
258
+ assertSummary: (summary) => {
259
+ expect(summary.isCacheStale).toBe(false);
260
+ expect(summary.writeReason).toBeUndefined();
261
+ },
262
+ });
263
+ });
264
+ it('matches email pointer discovery behavior', async () => {
265
+ await expectV2ToMatchOriginal({
266
+ incomingIdentity: {
267
+ traits: { emails: ['pointer@example.com'] },
268
+ },
269
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.EMAIL_POINTER_DISCOVERY,
270
+ setupMocks: () => {
271
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
272
+ pk: `email#${pixelId}#pointer@example.com`,
273
+ pixelId,
274
+ identityId: 'discovered-id',
275
+ gsi1pk: 'discovered-id',
276
+ updatedAt: '2026-01-01T00:00:00.000Z',
277
+ });
278
+ },
279
+ assertSummary: (summary) => {
280
+ expect(summary.didLookupByEmail).toBe(true);
281
+ expect(summary.discoveredIdentityId).toBe('discovered-id');
282
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.EMAIL_POINTER_DISCOVERY);
283
+ },
284
+ });
285
+ });
286
+ it('matches email pointer miss behavior', async () => {
287
+ await expectV2ToMatchOriginal({
288
+ incomingIdentity: {
289
+ traits: { emails: ['missing-pointer@example.com'] },
290
+ },
291
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.EMAIL_POINTER_MISS,
292
+ setupMocks: () => {
293
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
294
+ },
295
+ assertSummary: (summary) => {
296
+ expect(summary.didLookupByEmail).toBe(true);
297
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
298
+ },
299
+ });
300
+ });
301
+ it('matches no-email miss behavior without reading DynamoDB', async () => {
302
+ await expectV2ToMatchOriginal({
303
+ incomingIdentity: {
304
+ traits: { userIds: ['user-only'] },
305
+ },
306
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.EMAIL_POINTER_MISS,
307
+ setupMocks: () => undefined,
308
+ assertSummary: (summary) => {
309
+ expect(summary.didLookupByEmail).toBe(false);
310
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
311
+ },
312
+ });
313
+ });
314
+ it('matches fail-open behavior when primary lookup throws', async () => {
315
+ await expectV2ToMatchOriginal({
316
+ incomingIdentity: {
317
+ identityId: 'throws-primary',
318
+ traits: { emails: ['throws@example.com'] },
319
+ },
320
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_MISS,
321
+ setupMocks: () => {
322
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('dynamo failed'));
323
+ },
324
+ assertSummary: (summary) => {
325
+ expect(summary.errorMessage).toBeUndefined();
326
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
327
+ },
328
+ });
329
+ });
330
+ it('matches fail-open behavior when email lookup throws', async () => {
331
+ await expectV2ToMatchOriginal({
332
+ incomingIdentity: {
333
+ traits: { emails: ['throws-email@example.com'] },
334
+ },
335
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.EMAIL_POINTER_MISS,
336
+ setupMocks: () => {
337
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('email lookup dynamo failed'));
338
+ },
339
+ assertSummary: (summary) => {
340
+ expect(summary.didLookupByEmail).toBe(true);
341
+ expect(summary.errorMessage).toBeUndefined();
342
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
343
+ expect(summary.discoveredIdentityId).toBeUndefined();
344
+ },
345
+ });
346
+ });
347
+ it('matches primary cache hit with null traits on cached identity', async () => {
348
+ const cachedIdentity = {
349
+ identityId: 'null-traits-primary',
350
+ traits: null,
351
+ };
352
+ await expectV2ToMatchOriginal({
353
+ incomingIdentity: {
354
+ identityId: 'null-traits-primary',
355
+ traits: { emails: ['incoming@example.com'] },
356
+ },
357
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_STALE,
358
+ setupMocks: () => {
359
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
360
+ },
361
+ assertSummary: (summary) => {
362
+ expect(summary.didLookupByIdentityId).toBe(true);
363
+ expect(summary.isCacheStale).toBe(true);
364
+ expect(summary.cachedIdentity).toEqual(cachedIdentity);
365
+ expect(summary.mergedIdentity?.traits?.emails).toContain('incoming@example.com');
366
+ },
367
+ });
368
+ });
369
+ it('matches primary cache hit with empty traits object on cached identity', async () => {
370
+ const cachedIdentity = {
371
+ identityId: 'empty-traits-primary',
372
+ traits: {},
373
+ };
374
+ await expectV2ToMatchOriginal({
375
+ incomingIdentity: {
376
+ identityId: 'empty-traits-primary',
377
+ traits: { emails: ['new@example.com'] },
378
+ },
379
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_STALE,
380
+ setupMocks: () => {
381
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
382
+ },
383
+ assertSummary: (summary) => {
384
+ expect(summary.didLookupByIdentityId).toBe(true);
385
+ expect(summary.isCacheStale).toBe(true);
386
+ expect(summary.mergedIdentity?.traits?.emails).toContain('new@example.com');
387
+ },
388
+ });
389
+ });
390
+ it('matches primary cache hit when incoming has empty traits (subset of cached)', async () => {
391
+ const cachedIdentity = {
392
+ identityId: 'incoming-empty-traits',
393
+ traits: { emails: ['cached@example.com'], phones: ['+1234567890'] },
394
+ };
395
+ await expectV2ToMatchOriginal({
396
+ incomingIdentity: {
397
+ identityId: 'incoming-empty-traits',
398
+ traits: {},
399
+ },
400
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
401
+ setupMocks: () => {
402
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
403
+ },
404
+ assertSummary: (summary) => {
405
+ expect(summary.didLookupByIdentityId).toBe(true);
406
+ expect(summary.isCacheStale).toBe(false);
407
+ expect(summary.isDeepEqual).toBe(true);
408
+ expect(summary.mergedIdentity?.traits?.emails).toContain('cached@example.com');
409
+ expect(summary.mergedIdentity?.traits?.phones).toContain('+1234567890');
410
+ },
411
+ });
412
+ });
413
+ it('matches timestamp stripping behavior - addresses differ only by timestamp', async () => {
414
+ const cachedIdentity = {
415
+ identityId: 'timestamp-diff-primary',
416
+ traits: {
417
+ emails: ['timestamp@example.com'],
418
+ addresses: [
419
+ { city: 'Boston', s: at_tracking_event_types_1.ADDRESS_INFO_SOURCE.INPUT, cra: 1704067200000, upa: 1704067200000 },
420
+ ],
421
+ },
422
+ };
423
+ const incomingIdentity = {
424
+ identityId: 'timestamp-diff-primary',
425
+ traits: {
426
+ emails: ['timestamp@example.com'],
427
+ addresses: [
428
+ { city: 'Boston', s: at_tracking_event_types_1.ADDRESS_INFO_SOURCE.INPUT, cra: 1717200000000, upa: 1718409600000 },
429
+ ],
430
+ },
431
+ };
432
+ await expectV2ToMatchOriginal({
433
+ incomingIdentity,
434
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
435
+ setupMocks: () => {
436
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
437
+ },
438
+ assertSummary: (summary) => {
439
+ expect(summary.didLookupByIdentityId).toBe(true);
440
+ expect(summary.isDeepEqual).toBe(true);
441
+ expect(summary.isCacheStale).toBe(false);
442
+ expect(summary.writeReason).toBeUndefined();
443
+ },
444
+ });
445
+ });
446
+ it('verifies summary fields populated on primary cache hit', async () => {
447
+ const cachedIdentity = {
448
+ identityId: 'summary-hit-primary',
449
+ traits: { emails: ['summary-hit@example.com'], phones: ['+1111111111'] },
450
+ };
451
+ const incomingIdentity = {
452
+ identityId: 'summary-hit-primary',
453
+ traits: { emails: ['summary-hit@example.com'] },
454
+ };
455
+ const { v2Run } = await expectV2ToMatchOriginal({
456
+ incomingIdentity,
457
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
458
+ setupMocks: () => {
459
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
460
+ },
461
+ assertSummary: (summary) => {
462
+ expect(summary.cachedIdentity).toEqual(cachedIdentity);
463
+ expect(summary.incomingIdentity).toEqual(incomingIdentity);
464
+ expect(summary.mergedIdentity).toBeDefined();
465
+ expect(summary.mergedIdentity?.traits?.emails).toContain('summary-hit@example.com');
466
+ expect(summary.mergedIdentity?.traits?.phones).toContain('+1111111111');
467
+ expect(summary.resolvedIdentity).toEqual(summary.mergedIdentity);
468
+ expect(summary.incomingClick).toBeNull();
469
+ expect(summary.cachedClick).toBeNull();
470
+ expect(summary.mergedClick).toBeNull();
471
+ },
472
+ });
473
+ const cacheResult = v2Run.cacheResult;
474
+ expect(cacheResult.resolvedIdentity).toEqual(v2Run.summary.mergedIdentity);
475
+ expect(cacheResult.isCacheStale).toBe(false);
476
+ expect(cacheResult.cachedIdentity).toEqual(cachedIdentity);
477
+ expect(cacheResult.staleTracker).toBeUndefined();
478
+ expect(cacheResult.writeReason).toBeUndefined();
479
+ });
480
+ it('verifies staleTracker present only when stale', async () => {
481
+ const cachedIdentity = {
482
+ identityId: 'stale-tracker-primary',
483
+ traits: { emails: ['old@example.com'] },
484
+ };
485
+ const staleIncoming = {
486
+ identityId: 'stale-tracker-primary',
487
+ traits: { emails: ['old@example.com', 'new@example.com'] },
488
+ };
489
+ const { v2Run: staleRun } = await expectV2ToMatchOriginal({
490
+ incomingIdentity: staleIncoming,
491
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_STALE,
492
+ setupMocks: () => {
493
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
494
+ },
495
+ assertSummary: (summary) => {
496
+ expect(summary.isCacheStale).toBe(true);
497
+ },
498
+ });
499
+ const staleCacheResult = staleRun.cacheResult;
500
+ expect(staleCacheResult.staleTracker).toEqual(expect.objectContaining({
501
+ isCacheStale: true,
502
+ isEmailsStale: true,
503
+ }));
504
+ const freshIncoming = {
505
+ identityId: 'stale-tracker-primary',
506
+ traits: { emails: ['old@example.com'] },
507
+ };
508
+ const { v2Run: freshRun } = await expectV2ToMatchOriginal({
509
+ incomingIdentity: freshIncoming,
510
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
511
+ setupMocks: () => {
512
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
513
+ },
514
+ assertSummary: (summary) => {
515
+ expect(summary.isCacheStale).toBe(false);
516
+ },
517
+ });
518
+ const freshCacheResult = freshRun.cacheResult;
519
+ expect(freshCacheResult.staleTracker).toBeUndefined();
520
+ });
521
+ it('verifies summary fields populated on primary cache stale', async () => {
522
+ const cachedIdentity = {
523
+ identityId: 'summary-stale-primary',
524
+ traits: { emails: ['old@example.com'] },
525
+ };
526
+ const incomingIdentity = {
527
+ identityId: 'summary-stale-primary',
528
+ traits: { emails: ['old@example.com', 'new@example.com'], phones: ['+1234567890'] },
529
+ };
530
+ await expectV2ToMatchOriginal({
531
+ incomingIdentity,
532
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_STALE,
533
+ setupMocks: () => {
534
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
535
+ },
536
+ assertSummary: (summary) => {
537
+ expect(summary.cachedIdentity).toEqual(cachedIdentity);
538
+ expect(summary.incomingIdentity).toEqual(incomingIdentity);
539
+ expect(summary.mergedIdentity).toBeDefined();
540
+ expect(summary.mergedIdentity?.traits?.emails).toContain('old@example.com');
541
+ expect(summary.mergedIdentity?.traits?.emails).toContain('new@example.com');
542
+ expect(summary.mergedIdentity?.traits?.phones).toContain('+1234567890');
543
+ expect(summary.resolvedIdentity).toEqual(summary.mergedIdentity);
544
+ expect(summary.staleTier).toBeDefined();
545
+ expect(summary.staleTraits.length).toBeGreaterThan(0);
546
+ },
547
+ });
548
+ });
549
+ it('matches cache miss summary with correct flags', async () => {
550
+ await expectV2ToMatchOriginal({
551
+ incomingIdentity: {
552
+ traits: { userIds: ['user-only-no-email'] },
553
+ },
554
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.EMAIL_POINTER_MISS,
555
+ setupMocks: () => undefined,
556
+ assertSummary: (summary) => {
557
+ expect(summary.didLookupByIdentityId).toBe(false);
558
+ expect(summary.didLookupByEmail).toBe(false);
559
+ expect(summary.isCacheStale).toBe(true);
560
+ expect(summary.writeReason).toBe(types_js_1.CACHE_WRITE_REASON.CACHE_MISS);
561
+ expect(summary.cachedIdentity).toBeUndefined();
562
+ expect(summary.mergedIdentity).toBeUndefined();
563
+ expect(summary.resolvedIdentity).toBeUndefined();
564
+ expect(summary.discoveredIdentityId).toBeUndefined();
565
+ expect(summary.staleTraits).toEqual([]);
566
+ },
567
+ });
568
+ });
569
+ it('matches email pointer discovery summary fields', async () => {
570
+ const incomingIdentity = {
571
+ traits: { emails: ['discovery-summary@example.com'], phones: ['+9999999999'] },
572
+ };
573
+ await expectV2ToMatchOriginal({
574
+ incomingIdentity,
575
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.EMAIL_POINTER_DISCOVERY,
576
+ setupMocks: () => {
577
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
578
+ pk: `email#${pixelId}#discovery-summary@example.com`,
579
+ pixelId,
580
+ identityId: 'discovered-summary-id',
581
+ gsi1pk: 'discovered-summary-id',
582
+ updatedAt: '2026-01-01T00:00:00.000Z',
583
+ });
584
+ },
585
+ assertSummary: (summary) => {
586
+ expect(summary.didLookupByEmail).toBe(true);
587
+ expect(summary.discoveredIdentityId).toBe('discovered-summary-id');
588
+ expect(summary.resolvedIdentity?.identityId).toBe('discovered-summary-id');
589
+ expect(summary.resolvedIdentity?.traits?.emails).toContain('discovery-summary@example.com');
590
+ expect(summary.resolvedIdentity?.traits?.phones).toContain('+9999999999');
591
+ expect(summary.cachedIdentity).toBeUndefined();
592
+ expect(summary.mergedIdentity).toBeUndefined();
593
+ },
594
+ });
595
+ });
596
+ it('matches volatile-only with source field changes treated as cache hit', async () => {
597
+ const cachedIdentity = {
598
+ identityId: 'volatile-source-primary',
599
+ traits: {
600
+ emails: ['volatile-source@example.com'],
601
+ addresses: [{ city: 'Boston', s: at_tracking_event_types_1.ADDRESS_INFO_SOURCE.INPUT }],
602
+ ipAddress: '192.168.1.1',
603
+ userAgent: 'Mozilla/5.0 Old',
604
+ },
605
+ };
606
+ const incomingIdentity = {
607
+ identityId: 'volatile-source-primary',
608
+ traits: {
609
+ emails: ['volatile-source@example.com'],
610
+ addresses: [{ city: 'Boston', s: at_tracking_event_types_1.ADDRESS_INFO_SOURCE.INPUT }],
611
+ ipAddress: '10.0.0.1',
612
+ userAgent: 'Mozilla/5.0 New',
613
+ },
614
+ };
615
+ await expectV2ToMatchOriginal({
616
+ incomingIdentity,
617
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
618
+ setupMocks: () => {
619
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
620
+ },
621
+ assertSummary: (summary) => {
622
+ expect(summary.isCacheStale).toBe(false);
623
+ expect(summary.writeReason).toBeUndefined();
624
+ expect(summary.skipNeptune).toBeUndefined();
625
+ },
626
+ });
627
+ });
628
+ it('matches skipNeptune undefined when cache is fresh', async () => {
629
+ const cachedIdentity = {
630
+ identityId: 'skip-neptune-fresh',
631
+ traits: { emails: ['fresh@example.com'] },
632
+ };
633
+ await expectV2ToMatchOriginal({
634
+ incomingIdentity: deepClone(cachedIdentity),
635
+ expectedOutcome: types_js_1.IDENTITY_CACHE_LOOKUP_OUTCOME.PRIMARY_CACHE_HIT,
636
+ setupMocks: () => {
637
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(buildCachedRecord(cachedIdentity));
638
+ },
639
+ assertSummary: (summary) => {
640
+ expect(summary.isCacheStale).toBe(false);
641
+ expect(summary.skipNeptune).toBeUndefined();
642
+ },
643
+ });
644
+ });
645
+ });
646
+ //# sourceMappingURL=identity-cache-dynamodb-db-service-v2-comparison.spec.js.map