@usergist/feedback-react-native 0.2.0-beta.0 → 0.2.0-beta.2
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.
- package/CHANGELOG.md +18 -0
- package/README.md +65 -3
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js +51 -23
- package/dist/Provider.js.map +1 -1
- package/dist/Push.d.ts +2 -0
- package/dist/Push.d.ts.map +1 -1
- package/dist/Push.js +41 -2
- package/dist/Push.js.map +1 -1
- package/dist/UserGist.d.ts +17 -2
- package/dist/UserGist.d.ts.map +1 -1
- package/dist/UserGist.js +431 -139
- package/dist/UserGist.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/context.d.ts +1 -1
- package/dist/internal/context.js +1 -1
- package/dist/internal/engine.d.ts +16 -2
- package/dist/internal/engine.d.ts.map +1 -1
- package/dist/internal/engine.js +278 -76
- package/dist/internal/engine.js.map +1 -1
- package/dist/internal/events.d.ts +6 -1
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +39 -18
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/identity-lifecycle.d.ts +16 -0
- package/dist/internal/identity-lifecycle.d.ts.map +1 -0
- package/dist/internal/identity-lifecycle.js +96 -0
- package/dist/internal/identity-lifecycle.js.map +1 -0
- package/dist/internal/inapp-rules-cache.d.ts +1 -0
- package/dist/internal/inapp-rules-cache.d.ts.map +1 -1
- package/dist/internal/inapp-rules-cache.js +7 -0
- package/dist/internal/inapp-rules-cache.js.map +1 -1
- package/dist/internal/mutation-queue.d.ts +2 -2
- package/dist/internal/mutation-queue.d.ts.map +1 -1
- package/dist/internal/mutation-queue.js +43 -5
- package/dist/internal/mutation-queue.js.map +1 -1
- package/dist/internal/push-subscription.d.ts +11 -0
- package/dist/internal/push-subscription.d.ts.map +1 -0
- package/dist/internal/push-subscription.js +20 -0
- package/dist/internal/push-subscription.js.map +1 -0
- package/dist/internal/rules-cache.d.ts +1 -0
- package/dist/internal/rules-cache.d.ts.map +1 -1
- package/dist/internal/rules-cache.js +7 -0
- package/dist/internal/rules-cache.js.map +1 -1
- package/dist/internal/session-revocations.d.ts +5 -0
- package/dist/internal/session-revocations.d.ts.map +1 -0
- package/dist/internal/session-revocations.js +58 -0
- package/dist/internal/session-revocations.js.map +1 -0
- package/dist/internal/storage.d.ts +2 -0
- package/dist/internal/storage.d.ts.map +1 -1
- package/dist/internal/storage.js +75 -60
- package/dist/internal/storage.js.map +1 -1
- package/dist/internal/survey-invitations.d.ts +25 -0
- package/dist/internal/survey-invitations.d.ts.map +1 -0
- package/dist/internal/survey-invitations.js +54 -0
- package/dist/internal/survey-invitations.js.map +1 -0
- package/dist/internal/survey-matcher.d.ts +1 -0
- package/dist/internal/survey-matcher.d.ts.map +1 -1
- package/dist/internal/survey-matcher.js +3 -0
- package/dist/internal/survey-matcher.js.map +1 -1
- package/dist/internal/survey-rules-cache.d.ts +1 -0
- package/dist/internal/survey-rules-cache.d.ts.map +1 -1
- package/dist/internal/survey-rules-cache.js +7 -0
- package/dist/internal/survey-rules-cache.js.map +1 -1
- package/dist/internal/survey-store.d.ts +9 -2
- package/dist/internal/survey-store.d.ts.map +1 -1
- package/dist/internal/survey-store.js +37 -16
- package/dist/internal/survey-store.js.map +1 -1
- package/dist/internal/transport.d.ts +17 -4
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +40 -19
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/types.d.ts +2 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/internal/user-state.d.ts +2 -1
- package/dist/internal/user-state.d.ts.map +1 -1
- package/dist/internal/user-state.js +3 -2
- package/dist/internal/user-state.js.map +1 -1
- package/dist/ui/SurveyView.d.ts.map +1 -1
- package/dist/ui/SurveyView.js +5 -5
- package/dist/ui/SurveyView.js.map +1 -1
- package/package.json +2 -2
package/dist/internal/engine.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { rememberRevocation, drainRevocations } from './session-revocations.js';
|
|
2
|
+
import { identityChanged, recoverIdentity } from './identity-lifecycle.js';
|
|
3
|
+
import { PresentationGate } from '@usergist/sdk-core/mobile';
|
|
4
|
+
import { APP_OPEN_EVENT_NAME, APP_VERSION_CHANGED_EVENT_NAME, } from '@usergist/sdk-core/mobile';
|
|
4
5
|
import { STORAGE_KEYS, createStorageScope, } from './storage.js';
|
|
5
|
-
import { createIdentityManager, generateEventId } from './identity.js';
|
|
6
|
+
import { createIdentityManager, generateEventId, } from './identity.js';
|
|
6
7
|
import { createConsentManager } from './consent.js';
|
|
7
8
|
import { createEventQueue } from './queue.js';
|
|
8
|
-
import { createTransport, PermanentHttpError } from './transport.js';
|
|
9
|
+
import { createTransport, PermanentHttpError, } from './transport.js';
|
|
9
10
|
import { createRulesCache } from './rules-cache.js';
|
|
10
11
|
import { createSurveyRulesCache, } from './survey-rules-cache.js';
|
|
11
|
-
import { createFrequencyCapManager } from './frequency-cap.js';
|
|
12
|
+
import { createFrequencyCapManager, } from './frequency-cap.js';
|
|
12
13
|
import { createUserStateStore } from './user-state.js';
|
|
13
14
|
import { createTriggerMatcher } from './trigger-matcher.js';
|
|
14
15
|
import { createSurveyMatcher } from './survey-matcher.js';
|
|
15
|
-
import {
|
|
16
|
+
import { createSurveyInvitations } from './survey-invitations.js';
|
|
17
|
+
import { createInAppRulesCache, } from './inapp-rules-cache.js';
|
|
16
18
|
import { createInAppMatcher } from './inapp-matcher.js';
|
|
17
19
|
import { createLifecycleManager } from './lifecycle.js';
|
|
18
20
|
import { createContextProvider } from './context.js';
|
|
@@ -37,7 +39,9 @@ const DEFAULTS = {
|
|
|
37
39
|
debug: false,
|
|
38
40
|
};
|
|
39
41
|
export function resolveConfig(config) {
|
|
40
|
-
if (!config ||
|
|
42
|
+
if (!config ||
|
|
43
|
+
typeof config.writeKey !== 'string' ||
|
|
44
|
+
config.writeKey.length === 0) {
|
|
41
45
|
throw new Error('UserGist.init requires a writeKey');
|
|
42
46
|
}
|
|
43
47
|
const environment = config.environment ?? DEFAULTS.environment;
|
|
@@ -57,22 +61,28 @@ export function resolveConfig(config) {
|
|
|
57
61
|
appVersion: config.appVersion?.trim() || null,
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
|
-
export function createEngine(config) {
|
|
64
|
+
export function createEngine(config, prepareSurveys) {
|
|
61
65
|
const resolved = resolveConfig(config);
|
|
62
66
|
setDebugEnabled(resolved.debug);
|
|
63
67
|
const storage = createStorageScope(resolved.writeKey);
|
|
64
68
|
const identity = createIdentityManager(storage);
|
|
69
|
+
const surveyInvitations = createSurveyInvitations(storage, () => identity.get());
|
|
65
70
|
const consent = createConsentManager(storage);
|
|
66
71
|
const queue = createEventQueue(storage, resolved.maxQueueSize);
|
|
67
72
|
const mutations = createMutationQueue(storage);
|
|
68
73
|
const localInstructionDedupe = createLocalInstructionDedupe(storage);
|
|
69
|
-
const transport = createTransport({
|
|
74
|
+
const transport = createTransport({
|
|
75
|
+
writeKey: resolved.writeKey,
|
|
76
|
+
apiUrl: resolved.apiUrl,
|
|
77
|
+
prepareSurveys,
|
|
78
|
+
});
|
|
70
79
|
const rules = createRulesCache(storage, transport, resolved.triggerSyncIntervalMs);
|
|
71
80
|
const surveyRules = createSurveyRulesCache(storage, transport, resolved.triggerSyncIntervalMs);
|
|
72
81
|
const inAppRules = createInAppRulesCache(storage, transport, resolved.triggerSyncIntervalMs);
|
|
73
82
|
const caps = createFrequencyCapManager(storage);
|
|
74
83
|
const userState = createUserStateStore(storage);
|
|
75
|
-
const
|
|
84
|
+
const presentation = new PresentationGate(config.presentationPaused ?? false);
|
|
85
|
+
const events = createEventBus(presentation);
|
|
76
86
|
const matcher = createTriggerMatcher({
|
|
77
87
|
rulesCache: rules,
|
|
78
88
|
frequencyCaps: caps,
|
|
@@ -117,6 +127,7 @@ export function createEngine(config) {
|
|
|
117
127
|
},
|
|
118
128
|
onSyncTick: () => {
|
|
119
129
|
const e = eng();
|
|
130
|
+
void drainRevocations(e).catch(error => reportError('session cleanup deferred', error));
|
|
120
131
|
void refreshTargetingRules(e);
|
|
121
132
|
void flushMutations(e);
|
|
122
133
|
void pollSurveyOffers(e);
|
|
@@ -139,10 +150,12 @@ export function createEngine(config) {
|
|
|
139
150
|
userState,
|
|
140
151
|
matcher,
|
|
141
152
|
surveyMatcher,
|
|
153
|
+
surveyInvitations,
|
|
142
154
|
inAppMatcher,
|
|
143
155
|
lifecycle,
|
|
144
156
|
context,
|
|
145
157
|
events,
|
|
158
|
+
presentation,
|
|
146
159
|
hydrated: false,
|
|
147
160
|
hydratingPromise: null,
|
|
148
161
|
flushTimer: null,
|
|
@@ -151,6 +164,7 @@ export function createEngine(config) {
|
|
|
151
164
|
subjectToken: null,
|
|
152
165
|
sessionPromise: null,
|
|
153
166
|
instructionPollPromise: null,
|
|
167
|
+
instructionDispatchPromise: null,
|
|
154
168
|
resetGeneration: 0,
|
|
155
169
|
resetting: false,
|
|
156
170
|
localInstructionDedupe,
|
|
@@ -161,6 +175,10 @@ export function createEngine(config) {
|
|
|
161
175
|
lastPushRegistrationAt: 0,
|
|
162
176
|
};
|
|
163
177
|
ref.value = engine;
|
|
178
|
+
transport.setAuthenticationHandler(() => { void recoverIdentity(engine).then(ok => { if (ok) {
|
|
179
|
+
void flushNow(engine);
|
|
180
|
+
void flushMutations(engine);
|
|
181
|
+
} }); });
|
|
164
182
|
queue.onOverflow((dropped) => {
|
|
165
183
|
debugLog('queue overflow', { dropped, queueSize: queue.size() });
|
|
166
184
|
});
|
|
@@ -168,8 +186,10 @@ export function createEngine(config) {
|
|
|
168
186
|
matcher.recordShown(payload.promptId, payload.shownAt);
|
|
169
187
|
});
|
|
170
188
|
consent.subscribe((s) => {
|
|
171
|
-
if (!s.analytics)
|
|
189
|
+
if (!s.analytics) {
|
|
172
190
|
engine.queue.removePurpose('analytics');
|
|
191
|
+
void engine.mutations.removePurpose('analytics');
|
|
192
|
+
}
|
|
173
193
|
if (!s.feedback) {
|
|
174
194
|
engine.queue.removePurpose('feedback');
|
|
175
195
|
void engine.mutations.removePurpose('feedback');
|
|
@@ -177,6 +197,7 @@ export function createEngine(config) {
|
|
|
177
197
|
}
|
|
178
198
|
if (!s.survey) {
|
|
179
199
|
void engine.mutations.removePurpose('survey');
|
|
200
|
+
void engine.surveyInvitations.clear().catch((error) => reportError('clear survey invitations failed', error));
|
|
180
201
|
engine.surveyMatcher.resetPending();
|
|
181
202
|
}
|
|
182
203
|
if (s.feedback || s.analytics)
|
|
@@ -202,8 +223,10 @@ export async function ensureHydrated(engine) {
|
|
|
202
223
|
engine.surveyRules.hydrate(),
|
|
203
224
|
engine.inAppRules.hydrate(),
|
|
204
225
|
engine.localInstructionDedupe.hydrate(),
|
|
205
|
-
engine.storage.getJson(STORAGE_KEYS.pushToken).then(token => {
|
|
206
|
-
|
|
226
|
+
engine.storage.getJson(STORAGE_KEYS.pushToken).then((token) => {
|
|
227
|
+
if (typeof token === 'string')
|
|
228
|
+
engine.lastPushToken = token;
|
|
229
|
+
}),
|
|
207
230
|
]);
|
|
208
231
|
await ensureSubjectSession(engine);
|
|
209
232
|
engine.hydrated = true;
|
|
@@ -222,10 +245,14 @@ export async function ensureHydrated(engine) {
|
|
|
222
245
|
return engine.hydratingPromise;
|
|
223
246
|
}
|
|
224
247
|
export async function ensureSubjectSession(engine) {
|
|
248
|
+
if (engine.resetting)
|
|
249
|
+
return;
|
|
250
|
+
void drainRevocations(engine).catch(error => reportError('session cleanup deferred', error));
|
|
225
251
|
if (engine.subjectToken)
|
|
226
252
|
return;
|
|
227
253
|
if (engine.sessionPromise)
|
|
228
254
|
return engine.sessionPromise;
|
|
255
|
+
const generation = engine.resetGeneration;
|
|
229
256
|
engine.sessionPromise = (async () => {
|
|
230
257
|
const id = engine.identity.get();
|
|
231
258
|
const persisted = await engine.storage.getJson(STORAGE_KEYS.subjectToken);
|
|
@@ -234,13 +261,25 @@ export async function ensureSubjectSession(engine) {
|
|
|
234
261
|
anonymousId: id.anonymousId,
|
|
235
262
|
...(persisted ? { currentToken: persisted } : {}),
|
|
236
263
|
});
|
|
264
|
+
if (generation !== engine.resetGeneration)
|
|
265
|
+
return;
|
|
266
|
+
identityChanged(engine, id.externalId ? 'identified' : 'anonymous');
|
|
267
|
+
if (generation !== engine.resetGeneration)
|
|
268
|
+
return;
|
|
237
269
|
engine.subjectToken = session.subjectToken;
|
|
238
270
|
engine.transport.setSubjectToken(session.subjectToken);
|
|
239
271
|
await engine.storage.setJson(STORAGE_KEYS.subjectToken, session.subjectToken);
|
|
240
272
|
}
|
|
241
273
|
catch (error) {
|
|
274
|
+
if (generation !== engine.resetGeneration)
|
|
275
|
+
return;
|
|
242
276
|
const mayRotate = error instanceof PermanentHttpError &&
|
|
243
277
|
(error.status === 401 || error.status === 403 || error.status === 409);
|
|
278
|
+
if (id.externalId) {
|
|
279
|
+
if (await recoverIdentity(engine))
|
|
280
|
+
return;
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
244
283
|
if (!mayRotate)
|
|
245
284
|
throw error;
|
|
246
285
|
// A lost/expired credential must never be replaced for the same known
|
|
@@ -248,8 +287,14 @@ export async function ensureSubjectSession(engine) {
|
|
|
248
287
|
// new server-bound subject instead.
|
|
249
288
|
if (persisted)
|
|
250
289
|
await engine.storage.remove(STORAGE_KEYS.subjectToken);
|
|
290
|
+
if (generation !== engine.resetGeneration)
|
|
291
|
+
return;
|
|
251
292
|
const rotated = await engine.identity.rotate();
|
|
252
|
-
const session = await engine.transport.session({
|
|
293
|
+
const session = await engine.transport.session({
|
|
294
|
+
anonymousId: rotated.anonymousId,
|
|
295
|
+
});
|
|
296
|
+
if (generation !== engine.resetGeneration)
|
|
297
|
+
return;
|
|
253
298
|
engine.subjectToken = session.subjectToken;
|
|
254
299
|
engine.transport.setSubjectToken(session.subjectToken);
|
|
255
300
|
await engine.storage.setJson(STORAGE_KEYS.subjectToken, session.subjectToken);
|
|
@@ -337,6 +382,11 @@ function instructionKey(type, refId, eventId) {
|
|
|
337
382
|
function rememberLocalInstruction(engine, key) {
|
|
338
383
|
engine.localInstructionDedupe.remember(key);
|
|
339
384
|
}
|
|
385
|
+
function needsImmediateDelivery(engine, eventName) {
|
|
386
|
+
return !!(engine.rules?.needsServer?.(eventName) ||
|
|
387
|
+
engine.surveyRules?.needsServer?.(eventName) ||
|
|
388
|
+
engine.inAppRules?.needsServer?.(eventName));
|
|
389
|
+
}
|
|
340
390
|
export function scheduleFlush(engine) {
|
|
341
391
|
if (engine.flushTimer)
|
|
342
392
|
return;
|
|
@@ -356,11 +406,17 @@ export async function flushNow(engine) {
|
|
|
356
406
|
return engine.flushPromise;
|
|
357
407
|
}
|
|
358
408
|
async function performFlush(engine) {
|
|
409
|
+
void engine.retryPushRegistration?.().catch(error => reportError('push registration deferred', error));
|
|
410
|
+
let requestedImmediateDelivery = false;
|
|
359
411
|
try {
|
|
360
412
|
await ensureHydrated(engine);
|
|
361
|
-
while (engine.queue.size() > 0) {
|
|
413
|
+
while (!engine.resetting && engine.queue.size() > 0) {
|
|
362
414
|
const consent = engine.consent.get();
|
|
363
|
-
const allowed = engine.queue
|
|
415
|
+
const allowed = engine.queue
|
|
416
|
+
.snapshot()
|
|
417
|
+
.filter((event) => event.purpose === 'analytics'
|
|
418
|
+
? consent.analytics
|
|
419
|
+
: consent.feedback);
|
|
364
420
|
const firstAllowed = allowed[0];
|
|
365
421
|
if (!firstAllowed)
|
|
366
422
|
break;
|
|
@@ -368,8 +424,15 @@ async function performFlush(engine) {
|
|
|
368
424
|
// post-identify events. The API deliberately rejects a batch whose
|
|
369
425
|
// event identities differ from its context, so drain one identity at a
|
|
370
426
|
// time while retaining FIFO order within that identity.
|
|
371
|
-
const
|
|
372
|
-
event.
|
|
427
|
+
const candidates = allowed
|
|
428
|
+
.filter((event) => event.anonymousId === firstAllowed.anonymousId &&
|
|
429
|
+
event.externalId === firstAllowed.externalId)
|
|
430
|
+
.slice(0, engine.config.flushBatchSize);
|
|
431
|
+
let deliveryCount = 0;
|
|
432
|
+
const overflow = candidates.findIndex(event => needsImmediateDelivery(engine, event.name) && ++deliveryCount > 20);
|
|
433
|
+
// Limit authorization work, not ordinary analytics rows. Keep earlier
|
|
434
|
+
// watch/property events in order without shrinking every batch to 20.
|
|
435
|
+
const batch = overflow < 0 ? candidates : candidates.slice(0, overflow);
|
|
373
436
|
if (batch.length === 0)
|
|
374
437
|
break;
|
|
375
438
|
const ingestEvents = batch.map((e) => ({
|
|
@@ -383,7 +446,16 @@ async function performFlush(engine) {
|
|
|
383
446
|
sdkVersion: engine.context.sdkVersion(),
|
|
384
447
|
platform: engine.context.platform(),
|
|
385
448
|
}));
|
|
449
|
+
const deliveryEventIds = batch
|
|
450
|
+
.filter((event) => needsImmediateDelivery(engine, event.name))
|
|
451
|
+
.map((event) => event.eventId);
|
|
452
|
+
requestedImmediateDelivery ||= deliveryEventIds.length > 0;
|
|
453
|
+
const deliveryGeneration = engine.resetGeneration;
|
|
454
|
+
const deliverySubjectToken = engine.subjectToken;
|
|
386
455
|
const payload = {
|
|
456
|
+
...(deliveryEventIds.length
|
|
457
|
+
? { delivery: { eventIds: deliveryEventIds } }
|
|
458
|
+
: {}),
|
|
387
459
|
events: ingestEvents,
|
|
388
460
|
context: engine.context.build({
|
|
389
461
|
anonymousId: batch[0].anonymousId,
|
|
@@ -391,11 +463,18 @@ async function performFlush(engine) {
|
|
|
391
463
|
}),
|
|
392
464
|
};
|
|
393
465
|
try {
|
|
394
|
-
await engine.transport.ingest(payload);
|
|
466
|
+
const result = await engine.transport.ingest(payload);
|
|
467
|
+
if (engine.resetting ||
|
|
468
|
+
engine.resetGeneration !== deliveryGeneration ||
|
|
469
|
+
engine.subjectToken !== deliverySubjectToken)
|
|
470
|
+
return;
|
|
395
471
|
engine.queue.remove(batch.map((event) => event.eventId));
|
|
472
|
+
if (result.instructions?.length) {
|
|
473
|
+
await consumeInstructions(engine, result.instructions, false, deliveryGeneration, deliverySubjectToken);
|
|
474
|
+
}
|
|
396
475
|
}
|
|
397
476
|
catch (e) {
|
|
398
|
-
if (e instanceof PermanentHttpError) {
|
|
477
|
+
if (e instanceof PermanentHttpError && e.status !== 401) {
|
|
399
478
|
if (batch.length === 1) {
|
|
400
479
|
engine.queue.remove([batch[0].eventId]);
|
|
401
480
|
reportError('ingest event quarantined after permanent rejection', {
|
|
@@ -418,7 +497,7 @@ async function performFlush(engine) {
|
|
|
418
497
|
engine.queue.remove([first.eventId]);
|
|
419
498
|
}
|
|
420
499
|
catch (singleError) {
|
|
421
|
-
if (singleError instanceof PermanentHttpError) {
|
|
500
|
+
if (singleError instanceof PermanentHttpError && singleError.status !== 401) {
|
|
422
501
|
engine.queue.remove([first.eventId]);
|
|
423
502
|
reportError('ingest event quarantined after permanent rejection', {
|
|
424
503
|
eventId: first.eventId,
|
|
@@ -436,14 +515,14 @@ async function performFlush(engine) {
|
|
|
436
515
|
}
|
|
437
516
|
}
|
|
438
517
|
// After draining the queue, opportunistically pull pending survey offers.
|
|
439
|
-
|
|
518
|
+
void flushMutations(engine);
|
|
440
519
|
// Triggered surveys appear in the offer ledger after the server processes
|
|
441
520
|
// the event; the small delay gives NATS + the trigger engine time to
|
|
442
521
|
// run before we ask.
|
|
443
522
|
setTimeout(() => {
|
|
444
523
|
void pollSurveyOffers(engine);
|
|
445
524
|
void pollInstructions(engine);
|
|
446
|
-
}, 1500);
|
|
525
|
+
}, requestedImmediateDelivery ? 100 : 1500);
|
|
447
526
|
}
|
|
448
527
|
catch (e) {
|
|
449
528
|
reportError('flushNow failed', e);
|
|
@@ -469,14 +548,48 @@ async function performMutationFlush(engine) {
|
|
|
469
548
|
break;
|
|
470
549
|
const deliveryGeneration = engine.resetGeneration;
|
|
471
550
|
const consent = engine.consent.get();
|
|
551
|
+
if (mutation.purpose === 'analytics' && !consent.analytics)
|
|
552
|
+
break;
|
|
472
553
|
if (mutation.purpose === 'feedback' && !consent.feedback)
|
|
473
554
|
break;
|
|
474
555
|
if (mutation.purpose === 'survey' && !consent.survey)
|
|
475
556
|
break;
|
|
476
557
|
try {
|
|
477
|
-
if (mutation.kind === '
|
|
558
|
+
if (consent.analytics && (mutation.kind === 'identify' || mutation.kind === 'user-properties')) {
|
|
559
|
+
const current = engine.identity.get();
|
|
560
|
+
await engine.transport.consent({ anonymousId: current.anonymousId, externalId: current.externalId,
|
|
561
|
+
purposes: { analytics: consent.analytics, feedback: consent.feedback, push: consent.push, survey: consent.survey },
|
|
562
|
+
version: consent.version, effectiveAt: consent.updatedAt ?? new Date().toISOString(),
|
|
563
|
+
}, current.externalId && mutation.kind === 'identify' ? String(mutation.payload.subjectToken) : undefined);
|
|
564
|
+
if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
|
|
565
|
+
break;
|
|
566
|
+
if (mutation.kind === 'user-properties' && !engine.consent.allowsAnalytics())
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (mutation.kind === 'instruction-ack') {
|
|
570
|
+
await engine.transport.acknowledgeInstructions(mutation.payload.ids);
|
|
571
|
+
}
|
|
572
|
+
else if (mutation.kind === 'user-properties') {
|
|
573
|
+
const result = await engine.transport.userProperties(mutation.payload);
|
|
574
|
+
if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
|
|
575
|
+
break;
|
|
576
|
+
const set = mutation.payload.set;
|
|
577
|
+
const unset = mutation.payload.unset;
|
|
578
|
+
engine.userState.mergeProperties(Object.fromEntries(Object.entries(set ?? {}).filter(([key]) => !result.filteredKeys.includes(key))), unset);
|
|
579
|
+
if (result.filteredKeys.length)
|
|
580
|
+
reportError('User properties filtered by app privacy settings', result.filteredKeys);
|
|
581
|
+
}
|
|
582
|
+
else if (mutation.kind === 'feedback-response') {
|
|
478
583
|
await engine.transport.submitResponse(mutation.payload);
|
|
479
584
|
}
|
|
585
|
+
else if (mutation.kind === 'survey-start') {
|
|
586
|
+
await engine.transport.surveyCreateAttempt(String(mutation.payload.surveyId), mutation.payload
|
|
587
|
+
.body);
|
|
588
|
+
}
|
|
589
|
+
else if (mutation.kind === 'survey-progress') {
|
|
590
|
+
await engine.transport.surveyUpdateProgress(String(mutation.payload.attemptId), mutation.payload
|
|
591
|
+
.body);
|
|
592
|
+
}
|
|
480
593
|
else if (mutation.kind === 'survey-complete') {
|
|
481
594
|
const attemptId = mutation.payload.attemptId;
|
|
482
595
|
if (typeof attemptId !== 'string')
|
|
@@ -499,30 +612,49 @@ async function performMutationFlush(engine) {
|
|
|
499
612
|
typeof externalId !== 'string')
|
|
500
613
|
throw new Error('invalid-identify-mutation');
|
|
501
614
|
try {
|
|
502
|
-
await engine.transport.identify({
|
|
615
|
+
const accepted = await engine.transport.identify({
|
|
503
616
|
anonymousId,
|
|
504
617
|
externalId,
|
|
505
|
-
...(
|
|
506
|
-
|
|
618
|
+
...(engine.subjectToken ? { previousSubjectToken: engine.subjectToken } : {}),
|
|
619
|
+
...(engine.consent.allowsAnalytics() && properties && typeof properties === 'object'
|
|
620
|
+
? {
|
|
621
|
+
properties: properties,
|
|
622
|
+
}
|
|
507
623
|
: {}),
|
|
508
624
|
}, subjectToken);
|
|
509
625
|
if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
|
|
510
626
|
break;
|
|
511
|
-
|
|
627
|
+
const sessionToken = accepted.subjectToken ?? subjectToken;
|
|
628
|
+
await engine.storage.setJsonStrict(STORAGE_KEYS.subjectToken, sessionToken);
|
|
512
629
|
if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
|
|
513
630
|
break;
|
|
514
|
-
engine.subjectToken =
|
|
515
|
-
engine.transport.setSubjectToken(
|
|
631
|
+
engine.subjectToken = sessionToken;
|
|
632
|
+
engine.transport.setSubjectToken(sessionToken);
|
|
633
|
+
if (engine.identity.get().externalId !== externalId)
|
|
634
|
+
engine.presentation.invalidate();
|
|
516
635
|
await engine.identity.setExternalId(externalId);
|
|
636
|
+
if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
|
|
637
|
+
break;
|
|
638
|
+
identityChanged(engine, 'identified');
|
|
639
|
+
void engine.retryPushRegistration?.().catch(error => reportError('push registration deferred', error));
|
|
517
640
|
if (properties && typeof properties === 'object') {
|
|
518
|
-
const
|
|
519
|
-
|
|
641
|
+
const supplied = properties;
|
|
642
|
+
const clean = accepted.filteredKeys
|
|
643
|
+
? Object.fromEntries(Object.entries(supplied).filter(([key]) => !accepted.filteredKeys.includes(key)))
|
|
644
|
+
: asEventProps(supplied) ?? {};
|
|
645
|
+
if (accepted.properties)
|
|
646
|
+
engine.userState.replaceProperties(accepted.properties);
|
|
647
|
+
else
|
|
648
|
+
engine.userState.mergeProperties(clean);
|
|
520
649
|
if (engine.consent.allowsAnalytics()) {
|
|
521
|
-
enqueueAndEvaluate(engine, '$identify', clean);
|
|
650
|
+
enqueueAndEvaluate(engine, '$identify', asEventProps(clean));
|
|
522
651
|
}
|
|
523
652
|
}
|
|
524
|
-
else
|
|
525
|
-
|
|
653
|
+
else {
|
|
654
|
+
if (accepted.properties)
|
|
655
|
+
engine.userState.replaceProperties(accepted.properties);
|
|
656
|
+
if (engine.consent.allowsAnalytics())
|
|
657
|
+
enqueueAndEvaluate(engine, '$identify', undefined);
|
|
526
658
|
}
|
|
527
659
|
}
|
|
528
660
|
catch (error) {
|
|
@@ -536,7 +668,9 @@ async function performMutationFlush(engine) {
|
|
|
536
668
|
catch (error) {
|
|
537
669
|
if (engine.resetting || engine.resetGeneration !== deliveryGeneration)
|
|
538
670
|
break;
|
|
539
|
-
if (error instanceof PermanentHttpError)
|
|
671
|
+
if (error instanceof PermanentHttpError && error.status === 401 && mutation.kind === 'identify')
|
|
672
|
+
identityChanged(engine, 'authentication-required');
|
|
673
|
+
if (error instanceof PermanentHttpError && error.status !== 401) {
|
|
540
674
|
await engine.mutations.remove(mutation.id);
|
|
541
675
|
permanentlyRejectedIds.add(mutation.id);
|
|
542
676
|
reportError('mutation quarantined after permanent rejection', {
|
|
@@ -570,6 +704,7 @@ export async function pollSurveyOffers(engine) {
|
|
|
570
704
|
lastPollAt = now;
|
|
571
705
|
if (!engine.consent.get().survey)
|
|
572
706
|
return;
|
|
707
|
+
const presentationIsValid = engine.presentation.validator('survey');
|
|
573
708
|
const id = engine.identity.get();
|
|
574
709
|
const res = await engine.transport.surveysAvailable({
|
|
575
710
|
anonymousId: id.anonymousId,
|
|
@@ -578,7 +713,8 @@ export async function pollSurveyOffers(engine) {
|
|
|
578
713
|
const surveys = res.surveys ?? [];
|
|
579
714
|
if (surveys.length === 0)
|
|
580
715
|
return;
|
|
581
|
-
const seen = (await engine.storage.getJson(SEEN_OFFERS_KEY)) ??
|
|
716
|
+
const seen = (await engine.storage.getJson(SEEN_OFFERS_KEY)) ??
|
|
717
|
+
[];
|
|
582
718
|
const seenSet = new Set(seen);
|
|
583
719
|
const localIds = surveys
|
|
584
720
|
.map((survey) => survey.id)
|
|
@@ -587,6 +723,8 @@ export async function pollSurveyOffers(engine) {
|
|
|
587
723
|
!engine.locallyHandledSurveyIds.has(survey.id));
|
|
588
724
|
if (fresh.length === 0 && localIds.length === 0)
|
|
589
725
|
return;
|
|
726
|
+
if (!presentationIsValid() || !engine.consent.allowsSurvey())
|
|
727
|
+
return;
|
|
590
728
|
for (const s of fresh) {
|
|
591
729
|
engine.events.emit('surveyInvite', {
|
|
592
730
|
surveyId: s.id,
|
|
@@ -620,27 +758,82 @@ async function performInstructionPoll(engine) {
|
|
|
620
758
|
try {
|
|
621
759
|
await ensureHydrated(engine);
|
|
622
760
|
const after = (await engine.storage.getJson(STORAGE_KEYS.instructionCursor)) ?? 0;
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
const result = await engine.transport.instructions(after, {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (seenSet.has(instruction.id))
|
|
632
|
-
continue;
|
|
633
|
-
dispatchInstruction(engine, instruction.type, instruction.payload);
|
|
634
|
-
seenSet.add(instruction.id);
|
|
635
|
-
await engine.storage.setJsonStrict(STORAGE_KEYS.seenInstructions, [...seenSet].slice(-200));
|
|
636
|
-
}
|
|
637
|
-
await engine.storage.setJsonStrict(STORAGE_KEYS.instructionCursor, Math.max(after, ...handledIds));
|
|
638
|
-
await engine.transport.acknowledgeInstructions(handledIds);
|
|
761
|
+
const generation = engine.resetGeneration;
|
|
762
|
+
const subjectToken = engine.subjectToken;
|
|
763
|
+
const result = await engine.transport.instructions(after, {
|
|
764
|
+
anonymousId: engine.identity.get().anonymousId,
|
|
765
|
+
platform: engine.context.platform(),
|
|
766
|
+
sdkVersion: engine.context.sdkVersion(),
|
|
767
|
+
});
|
|
768
|
+
await consumeInstructions(engine, result.instructions, true, generation, subjectToken);
|
|
639
769
|
}
|
|
640
770
|
catch (error) {
|
|
641
771
|
reportError('instruction poll failed', error);
|
|
642
772
|
}
|
|
643
773
|
}
|
|
774
|
+
/** Serializes HTTP responses and polling. Event-scoped responses must never
|
|
775
|
+
* advance the global inbox cursor past older, still-undelivered instructions. */
|
|
776
|
+
export function consumeInstructions(engine, instructions, advanceCursor, generation = engine.resetGeneration, subjectToken = engine.subjectToken) {
|
|
777
|
+
const isCurrent = () => !engine.resetting &&
|
|
778
|
+
engine.resetGeneration === generation &&
|
|
779
|
+
engine.subjectToken === subjectToken;
|
|
780
|
+
const work = (engine.instructionDispatchPromise ?? Promise.resolve())
|
|
781
|
+
.catch(() => undefined)
|
|
782
|
+
.then(async () => {
|
|
783
|
+
if (!instructions.length || !isCurrent())
|
|
784
|
+
return;
|
|
785
|
+
const seen = new Set((await engine.storage.getJson(STORAGE_KEYS.seenInstructions)) ?? []);
|
|
786
|
+
if (!isCurrent())
|
|
787
|
+
return;
|
|
788
|
+
const handled = [];
|
|
789
|
+
for (const instruction of instructions) {
|
|
790
|
+
if (!isCurrent())
|
|
791
|
+
return;
|
|
792
|
+
handled.push(instruction.id);
|
|
793
|
+
if (seen.has(instruction.id))
|
|
794
|
+
continue;
|
|
795
|
+
const payload = instruction.payload;
|
|
796
|
+
if (instruction.type === 'survey.offer' && engine.consent.allowsSurvey() &&
|
|
797
|
+
typeof payload.surveyId === 'string' && typeof payload.name === 'string' &&
|
|
798
|
+
typeof payload.presentationId === 'string' && payload.survey && typeof payload.survey === 'object') {
|
|
799
|
+
// Persist before invoking the host callback or acknowledging delivery.
|
|
800
|
+
// A storage failure leaves the instruction available for retry.
|
|
801
|
+
await engine.surveyInvitations.remember({
|
|
802
|
+
surveyId: payload.surveyId,
|
|
803
|
+
presentationId: payload.presentationId,
|
|
804
|
+
source: typeof payload.source === 'string' ? payload.source : 'triggered',
|
|
805
|
+
survey: { ...payload.survey, presentationId: payload.presentationId },
|
|
806
|
+
attempt: payload.attempt,
|
|
807
|
+
expiresAt: Math.min(Date.parse(instruction.expiresAt), Date.now() + 24 * 60 * 60 * 1000),
|
|
808
|
+
});
|
|
809
|
+
if (!isCurrent())
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
dispatchInstruction(engine, instruction.type, instruction.payload);
|
|
813
|
+
seen.add(instruction.id);
|
|
814
|
+
await engine.storage.setJsonStrict(STORAGE_KEYS.seenInstructions, [...seen].slice(-200));
|
|
815
|
+
}
|
|
816
|
+
if (!isCurrent())
|
|
817
|
+
return;
|
|
818
|
+
// Persist receipts before advancing the cursor. Network retries must not
|
|
819
|
+
// hold the display/ingest lane or lose cap accounting after a restart.
|
|
820
|
+
await engine.mutations.enqueue('instruction-ack', 'essential', {
|
|
821
|
+
ids: handled,
|
|
822
|
+
});
|
|
823
|
+
if (!isCurrent())
|
|
824
|
+
return;
|
|
825
|
+
if (advanceCursor) {
|
|
826
|
+
const after = (await engine.storage.getJson(STORAGE_KEYS.instructionCursor)) ?? 0;
|
|
827
|
+
if (!isCurrent())
|
|
828
|
+
return;
|
|
829
|
+
await engine.storage.setJsonStrict(STORAGE_KEYS.instructionCursor, Math.max(after, ...handled));
|
|
830
|
+
}
|
|
831
|
+
if (isCurrent())
|
|
832
|
+
void flushMutations(engine);
|
|
833
|
+
});
|
|
834
|
+
engine.instructionDispatchPromise = work;
|
|
835
|
+
return work;
|
|
836
|
+
}
|
|
644
837
|
function dispatchInstruction(engine, type, payload) {
|
|
645
838
|
if (type === 'prompt.show') {
|
|
646
839
|
if (!engine.consent.allowsFeedback())
|
|
@@ -675,7 +868,8 @@ function dispatchInstruction(engine, type, payload) {
|
|
|
675
868
|
if (type === 'survey.offer') {
|
|
676
869
|
if (!engine.consent.allowsSurvey())
|
|
677
870
|
return;
|
|
678
|
-
if (typeof payload.surveyId !== 'string' ||
|
|
871
|
+
if (typeof payload.surveyId !== 'string' ||
|
|
872
|
+
typeof payload.name !== 'string')
|
|
679
873
|
return;
|
|
680
874
|
const triggerEventId = payload.triggerEventId;
|
|
681
875
|
if (typeof triggerEventId === 'string' &&
|
|
@@ -687,6 +881,13 @@ function dispatchInstruction(engine, type, payload) {
|
|
|
687
881
|
return;
|
|
688
882
|
}
|
|
689
883
|
engine.events.emit('surveyInvite', {
|
|
884
|
+
attempt: payload.attempt,
|
|
885
|
+
survey: payload.survey && typeof payload.survey === 'object'
|
|
886
|
+
? {
|
|
887
|
+
...payload.survey,
|
|
888
|
+
presentationId: payload.presentationId,
|
|
889
|
+
}
|
|
890
|
+
: undefined,
|
|
690
891
|
surveyId: payload.surveyId,
|
|
691
892
|
name: payload.name,
|
|
692
893
|
source: typeof payload.source === 'string' ? payload.source : 'triggered',
|
|
@@ -760,7 +961,8 @@ export function enqueueAndEvaluate(engine, eventName, properties, purpose = 'ana
|
|
|
760
961
|
engine.queue.enqueue(base);
|
|
761
962
|
engine.userState.recordEvent(eventName, now);
|
|
762
963
|
evaluateLocally(engine, eventName, base.eventId);
|
|
763
|
-
if (engine
|
|
964
|
+
if (needsImmediateDelivery(engine, eventName) ||
|
|
965
|
+
engine.queue.size() >= engine.config.flushBatchSize)
|
|
764
966
|
void flushNow(engine);
|
|
765
967
|
else
|
|
766
968
|
scheduleFlush(engine);
|
|
@@ -776,7 +978,8 @@ export function enqueueAndEvaluate(engine, eventName, properties, purpose = 'ana
|
|
|
776
978
|
engine.queue.enqueue(withId);
|
|
777
979
|
engine.userState.recordEvent(eventName, now);
|
|
778
980
|
evaluateLocally(engine, eventName, withId.eventId);
|
|
779
|
-
if (engine
|
|
981
|
+
if (needsImmediateDelivery(engine, eventName) ||
|
|
982
|
+
engine.queue.size() >= engine.config.flushBatchSize)
|
|
780
983
|
void flushNow(engine);
|
|
781
984
|
else
|
|
782
985
|
scheduleFlush(engine);
|
|
@@ -806,14 +1009,21 @@ function evaluateLocally(engine, eventName, eventId) {
|
|
|
806
1009
|
}
|
|
807
1010
|
export async function submitResponse(engine, payload, triggerEventName, trackInternal) {
|
|
808
1011
|
const id = engine.identity.get();
|
|
1012
|
+
const context = engine.context.build(id);
|
|
809
1013
|
const submitPayload = {
|
|
810
1014
|
idempotencyKey: generateEventId(),
|
|
811
1015
|
promptId: payload.promptId,
|
|
812
1016
|
anonymousId: id.anonymousId,
|
|
813
1017
|
externalId: id.externalId,
|
|
814
|
-
answers: payload.answers.map((a) => ({
|
|
1018
|
+
answers: payload.answers.map((a) => ({
|
|
1019
|
+
questionId: a.questionId,
|
|
1020
|
+
value: a.value,
|
|
1021
|
+
})),
|
|
815
1022
|
dismissed: payload.dismissed,
|
|
816
1023
|
latencyMs: payload.latencyMs,
|
|
1024
|
+
platform: context.platform,
|
|
1025
|
+
sdkVersion: context.sdkVersion,
|
|
1026
|
+
appVersion: context.appVersion,
|
|
817
1027
|
};
|
|
818
1028
|
engine.events.emit('response', payload);
|
|
819
1029
|
trackInternal('$feedback_response', {
|
|
@@ -837,28 +1047,15 @@ export async function clearAllState(engine) {
|
|
|
837
1047
|
pendingAppOpenConsent.delete(engine);
|
|
838
1048
|
engine.events.emit('resetSurfaces', undefined);
|
|
839
1049
|
engine.transport.cancelAll();
|
|
1050
|
+
engine.transport.setSubjectToken(null);
|
|
840
1051
|
await Promise.allSettled([
|
|
841
1052
|
engine.mutationFlushPromise ?? Promise.resolve(),
|
|
842
1053
|
engine.flushPromise ?? Promise.resolve(),
|
|
843
1054
|
engine.sessionPromise ?? Promise.resolve(),
|
|
844
1055
|
engine.instructionPollPromise ?? Promise.resolve(),
|
|
1056
|
+
engine.instructionDispatchPromise ?? Promise.resolve(),
|
|
845
1057
|
]);
|
|
846
|
-
|
|
847
|
-
// Revoke the old credential through an isolated transport so failures cannot
|
|
848
|
-
// open the live engine's circuit or delay the new anonymous session.
|
|
849
|
-
const subjectToken = engine.subjectToken;
|
|
850
|
-
if (subjectToken) {
|
|
851
|
-
const revocationTransport = createTransport({
|
|
852
|
-
writeKey: engine.config.writeKey,
|
|
853
|
-
apiUrl: engine.config.apiUrl,
|
|
854
|
-
});
|
|
855
|
-
revocationTransport.setSubjectToken(subjectToken);
|
|
856
|
-
void revocationTransport.revokeSession().catch((error) => {
|
|
857
|
-
debugLog('subject session revoke deferred', {
|
|
858
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
859
|
-
});
|
|
860
|
-
});
|
|
861
|
-
}
|
|
1058
|
+
await rememberRevocation(engine);
|
|
862
1059
|
if (engine.flushTimer) {
|
|
863
1060
|
clearTimeout(engine.flushTimer);
|
|
864
1061
|
engine.flushTimer = null;
|
|
@@ -875,9 +1072,9 @@ export async function clearAllState(engine) {
|
|
|
875
1072
|
engine.userState.clear(),
|
|
876
1073
|
engine.consent.clear(),
|
|
877
1074
|
engine.localInstructionDedupe.clear(),
|
|
1075
|
+
engine.surveyInvitations.clear(),
|
|
878
1076
|
]);
|
|
879
1077
|
await engine.storage.clearAll([
|
|
880
|
-
STORAGE_KEYS.identity,
|
|
881
1078
|
STORAGE_KEYS.consent,
|
|
882
1079
|
STORAGE_KEYS.queue,
|
|
883
1080
|
STORAGE_KEYS.rulesCache,
|
|
@@ -894,6 +1091,7 @@ export async function clearAllState(engine) {
|
|
|
894
1091
|
STORAGE_KEYS.pushToken,
|
|
895
1092
|
SEEN_OFFERS_KEY,
|
|
896
1093
|
]);
|
|
1094
|
+
await engine.storage.setJsonStrict(STORAGE_KEYS.subjectToken, null);
|
|
897
1095
|
engine.subjectToken = null;
|
|
898
1096
|
engine.transport.setSubjectToken(null);
|
|
899
1097
|
engine.locallyHandledSurveyIds.clear();
|
|
@@ -904,6 +1102,10 @@ export async function clearAllState(engine) {
|
|
|
904
1102
|
engine.lastPushRegistrationAt = 0;
|
|
905
1103
|
lastPollAt = 0;
|
|
906
1104
|
await engine.identity.hydrate();
|
|
907
|
-
|
|
1105
|
+
engine.pendingReset = undefined;
|
|
1106
|
+
identityChanged(engine, 'anonymous');
|
|
1107
|
+
void drainRevocations(engine).catch(error => reportError('session cleanup deferred', error));
|
|
1108
|
+
// Local logout completes independently of network availability.
|
|
1109
|
+
setTimeout(() => { void ensureSubjectSession(engine).catch(error => reportError('anonymous session deferred', error)); }, 0);
|
|
908
1110
|
}
|
|
909
1111
|
//# sourceMappingURL=engine.js.map
|