@hot-updater/test-utils 1.0.0-rc.36 → 1.0.0-rc.37
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/dist/index.mjs +246 -21
- package/package.json +10 -10
package/dist/index.mjs
CHANGED
|
@@ -2902,10 +2902,11 @@ const storage = {
|
|
|
2902
2902
|
resolveFileUrl: async (storageUri) => `https://storage.example.com/${encodeURIComponent(storageUri)}`
|
|
2903
2903
|
};
|
|
2904
2904
|
/**
|
|
2905
|
-
* Core's bundles, releases, channels, and a client API key, and
|
|
2905
|
+
* Core's bundles, releases, channels, and a client API key, and 26 Insights
|
|
2906
2906
|
* events: installs 1–24 ten minutes apart from T0, then install 1 again two
|
|
2907
|
-
* days later; and the day of history
|
|
2908
|
-
* holds, so a read that scans beyond its
|
|
2907
|
+
* days later, back on bundle-a and then on bundle-b; and the day of history
|
|
2908
|
+
* before T0, as a production database holds, so a read that scans beyond its
|
|
2909
|
+
* window examines more rows.
|
|
2909
2910
|
*/
|
|
2910
2911
|
const seed = async (database) => {
|
|
2911
2912
|
const core = database.core;
|
|
@@ -2951,8 +2952,17 @@ const seed = async (database) => {
|
|
|
2951
2952
|
for (let n = 1; n <= 24; n += 1) await api.insights.recordEvent(eventOf(n));
|
|
2952
2953
|
await api.insights.recordEvent(eventOf(25, {
|
|
2953
2954
|
install_id: "install-1",
|
|
2955
|
+
from_release_id: "release-b",
|
|
2956
|
+
from_bundle_id: "bundle-b",
|
|
2957
|
+
to_release_id: "release-a",
|
|
2958
|
+
to_bundle_id: "bundle-a",
|
|
2954
2959
|
received_at_ms: T0 + 2 * DAY
|
|
2955
2960
|
}));
|
|
2961
|
+
await api.insights.recordEvent(eventOf(26, {
|
|
2962
|
+
install_id: "install-1",
|
|
2963
|
+
user_id: "user-odd",
|
|
2964
|
+
received_at_ms: T0 + 2 * DAY + 6e5
|
|
2965
|
+
}));
|
|
2956
2966
|
for (let n = 1; n <= 72; n += 1) await api.insights.recordEvent(historyOf(n));
|
|
2957
2967
|
await api.insights.recordEvent(failureOf(1, 3));
|
|
2958
2968
|
await api.insights.recordEvent(failureOf(2, 5));
|
|
@@ -3386,11 +3396,16 @@ const READ_BUDGETS = [
|
|
|
3386
3396
|
}),
|
|
3387
3397
|
budget({
|
|
3388
3398
|
api: "record an insights event: 2 dependent rounds of batch gets and 1 write",
|
|
3389
|
-
read: ({ insights }) => insights.recordEvent(eventOf(
|
|
3399
|
+
read: ({ insights }) => insights.recordEvent(eventOf(27, {
|
|
3390
3400
|
install_id: "install-2",
|
|
3401
|
+
user_id: "user-even",
|
|
3402
|
+
from_release_id: "release-b",
|
|
3403
|
+
from_bundle_id: "bundle-b",
|
|
3404
|
+
to_release_id: "release-c",
|
|
3405
|
+
to_bundle_id: "bundle-c",
|
|
3391
3406
|
received_at_ms: T0 + 3 * HOUR
|
|
3392
3407
|
})),
|
|
3393
|
-
adapter: reads(
|
|
3408
|
+
adapter: reads(6, 14, 0, 0),
|
|
3394
3409
|
engine: {
|
|
3395
3410
|
calls: 2,
|
|
3396
3411
|
rows: 1
|
|
@@ -4261,20 +4276,31 @@ const registerInsightsModelTests = (state) => {
|
|
|
4261
4276
|
limit: 1
|
|
4262
4277
|
}), [expected]);
|
|
4263
4278
|
}
|
|
4264
|
-
|
|
4279
|
+
const firstSeen = {
|
|
4280
|
+
...unchanged,
|
|
4281
|
+
metadata: {
|
|
4282
|
+
...unchanged.metadata,
|
|
4283
|
+
change: {
|
|
4284
|
+
kinds: ["first_seen"],
|
|
4285
|
+
previous: null
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
};
|
|
4289
|
+
await expect(model.findLatestEvents({ installId: unchanged.install_id })).resolves.toEqual([firstSeen]);
|
|
4265
4290
|
await expectInsightsIndex(() => model.listEvents({
|
|
4266
4291
|
filter: { kind: "all" },
|
|
4267
4292
|
sinceMs: 100,
|
|
4268
4293
|
beforeReceivedAtMs: 200,
|
|
4269
4294
|
limit: 10
|
|
4270
4295
|
}), [
|
|
4296
|
+
firstSeen,
|
|
4271
4297
|
excluded[4],
|
|
4272
4298
|
recovered,
|
|
4273
4299
|
excluded[3],
|
|
4274
4300
|
excluded[2],
|
|
4275
4301
|
applied
|
|
4276
4302
|
]);
|
|
4277
|
-
await
|
|
4303
|
+
await expectInsightsIndex(() => model.countEvents({
|
|
4278
4304
|
filter: {
|
|
4279
4305
|
platform: "ios",
|
|
4280
4306
|
channel: "production",
|
|
@@ -4283,7 +4309,7 @@ const registerInsightsModelTests = (state) => {
|
|
|
4283
4309
|
},
|
|
4284
4310
|
sinceMs: 100,
|
|
4285
4311
|
beforeReceivedAtMs: 200
|
|
4286
|
-
}))
|
|
4312
|
+
}), 0);
|
|
4287
4313
|
await expectInsightsIndex(() => model.countLatestEvents({
|
|
4288
4314
|
platform: "ios",
|
|
4289
4315
|
channel: "production",
|
|
@@ -4444,7 +4470,9 @@ const registerInsightsModelTests = (state) => {
|
|
|
4444
4470
|
id: createBundleEventRowFixture("9804", 3e3).id,
|
|
4445
4471
|
type: "RECOVERED",
|
|
4446
4472
|
from_release_id: releaseB,
|
|
4473
|
+
from_bundle_id: base.to_bundle_id,
|
|
4447
4474
|
to_release_id: releaseA,
|
|
4475
|
+
to_bundle_id: base.from_bundle_id,
|
|
4448
4476
|
received_at_ms: 3e3
|
|
4449
4477
|
};
|
|
4450
4478
|
for (const event of [
|
|
@@ -4497,7 +4525,7 @@ const registerInsightsModelTests = (state) => {
|
|
|
4497
4525
|
await expectInsightsIndex(() => hourly({
|
|
4498
4526
|
...scope,
|
|
4499
4527
|
type: "RECOVERED",
|
|
4500
|
-
fromBundleId: base.
|
|
4528
|
+
fromBundleId: base.to_bundle_id
|
|
4501
4529
|
}), points(1, 0, 0));
|
|
4502
4530
|
const usage = await model.getAppUsage({
|
|
4503
4531
|
channel: "production",
|
|
@@ -4603,24 +4631,26 @@ const registerInsightsModelTests = (state) => {
|
|
|
4603
4631
|
});
|
|
4604
4632
|
it("filters installation movements before applying the page limit", async () => {
|
|
4605
4633
|
const model = state.getDatabase();
|
|
4634
|
+
const unrelated = createMovementEvent("712", 250, "UPDATE_APPLIED", "install-other");
|
|
4635
|
+
const applied = createMovementEvent("713", 200, "UPDATE_APPLIED", "install-target");
|
|
4636
|
+
const recovered = createMovementEvent("714", 100, "RECOVERED", "install-target");
|
|
4606
4637
|
const unchanged = {
|
|
4607
|
-
...
|
|
4638
|
+
...applied,
|
|
4639
|
+
id: createBundleEventRowFixture("711", 300).id,
|
|
4608
4640
|
type: "UNCHANGED",
|
|
4609
|
-
install_id: "install-target",
|
|
4610
4641
|
from_bundle_id: null,
|
|
4642
|
+
from_release_id: null,
|
|
4611
4643
|
metadata: {
|
|
4612
|
-
...
|
|
4644
|
+
...applied.metadata,
|
|
4613
4645
|
update_strategy: null
|
|
4614
|
-
}
|
|
4646
|
+
},
|
|
4647
|
+
received_at_ms: 300
|
|
4615
4648
|
};
|
|
4616
|
-
const unrelated = createMovementEvent("712", 250, "UPDATE_APPLIED", "install-other");
|
|
4617
|
-
const applied = createMovementEvent("713", 200, "UPDATE_APPLIED", "install-target");
|
|
4618
|
-
const recovered = createMovementEvent("714", 100, "RECOVERED", "install-target");
|
|
4619
4649
|
for (const row of [
|
|
4620
|
-
|
|
4650
|
+
recovered,
|
|
4621
4651
|
unrelated,
|
|
4622
4652
|
applied,
|
|
4623
|
-
|
|
4653
|
+
unchanged
|
|
4624
4654
|
]) await model.recordEvent({ event: row });
|
|
4625
4655
|
await expectInsightsIndex(() => model.listEvents({
|
|
4626
4656
|
filter: {
|
|
@@ -4643,6 +4673,200 @@ const registerInsightsModelTests = (state) => {
|
|
|
4643
4673
|
limit: 1
|
|
4644
4674
|
}), [recovered]);
|
|
4645
4675
|
});
|
|
4676
|
+
it("keeps an UNCHANGED report only when it changes what its installation runs, and counts the release it launched", async () => {
|
|
4677
|
+
const model = state.getDatabase();
|
|
4678
|
+
const day = 864e5;
|
|
4679
|
+
const releaseA = "00000000-0000-7000-8000-000000009a01";
|
|
4680
|
+
const releaseB = "00000000-0000-7000-8000-000000009a02";
|
|
4681
|
+
const base = createBundleEventRowFixture("9910", 1e3);
|
|
4682
|
+
const launch = (suffix, receivedAtMs, fields = {}) => ({
|
|
4683
|
+
...base,
|
|
4684
|
+
id: createBundleEventRowFixture(suffix, receivedAtMs).id,
|
|
4685
|
+
type: "UNCHANGED",
|
|
4686
|
+
install_id: "install-kept",
|
|
4687
|
+
from_bundle_id: null,
|
|
4688
|
+
from_release_id: null,
|
|
4689
|
+
to_bundle_id: base.from_bundle_id,
|
|
4690
|
+
to_release_id: releaseA,
|
|
4691
|
+
metadata: {
|
|
4692
|
+
...base.metadata,
|
|
4693
|
+
update_strategy: null
|
|
4694
|
+
},
|
|
4695
|
+
received_at_ms: receivedAtMs,
|
|
4696
|
+
...fields
|
|
4697
|
+
});
|
|
4698
|
+
const first = launch("9911", 1e3);
|
|
4699
|
+
const same = launch("9912", 86401e3);
|
|
4700
|
+
const moved = launch("9913", 86402e3, {
|
|
4701
|
+
to_bundle_id: base.to_bundle_id,
|
|
4702
|
+
to_release_id: releaseB
|
|
4703
|
+
});
|
|
4704
|
+
for (const row of [
|
|
4705
|
+
first,
|
|
4706
|
+
same,
|
|
4707
|
+
moved
|
|
4708
|
+
]) await record(model, row);
|
|
4709
|
+
await expectInsightsIndex(() => model.listEvents({
|
|
4710
|
+
filter: {
|
|
4711
|
+
kind: "installationMovement",
|
|
4712
|
+
installId: "install-kept"
|
|
4713
|
+
},
|
|
4714
|
+
beforeReceivedAtMs: 2 * day,
|
|
4715
|
+
limit: 10
|
|
4716
|
+
}), [{
|
|
4717
|
+
...moved,
|
|
4718
|
+
metadata: {
|
|
4719
|
+
...moved.metadata,
|
|
4720
|
+
implied_download: true,
|
|
4721
|
+
change: {
|
|
4722
|
+
kinds: ["bundle", "release"],
|
|
4723
|
+
previous: {
|
|
4724
|
+
bundle_id: base.from_bundle_id,
|
|
4725
|
+
release_id: releaseA,
|
|
4726
|
+
app_version: base.app_version,
|
|
4727
|
+
channel: base.channel
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
}, {
|
|
4732
|
+
...first,
|
|
4733
|
+
metadata: {
|
|
4734
|
+
...first.metadata,
|
|
4735
|
+
change: {
|
|
4736
|
+
kinds: ["first_seen"],
|
|
4737
|
+
previous: null
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
}]);
|
|
4741
|
+
await expectInsightsIndex(() => model.countEvents({
|
|
4742
|
+
filter: {
|
|
4743
|
+
platform: "ios",
|
|
4744
|
+
channel: "production",
|
|
4745
|
+
type: "UNCHANGED",
|
|
4746
|
+
toBundleId: base.to_bundle_id
|
|
4747
|
+
},
|
|
4748
|
+
sinceMs: 0,
|
|
4749
|
+
beforeReceivedAtMs: 2 * day
|
|
4750
|
+
}), 1);
|
|
4751
|
+
const activity = await model.getReleaseActivity({ releases: [{
|
|
4752
|
+
releaseId: releaseA,
|
|
4753
|
+
platform: "ios",
|
|
4754
|
+
channel: "production"
|
|
4755
|
+
}, {
|
|
4756
|
+
releaseId: releaseB,
|
|
4757
|
+
platform: "ios",
|
|
4758
|
+
channel: "production"
|
|
4759
|
+
}] });
|
|
4760
|
+
expect(activity.data.map(({ metrics }) => metrics)).toEqual([{
|
|
4761
|
+
downloads: 0,
|
|
4762
|
+
applies: 0,
|
|
4763
|
+
failedLaunches: 0
|
|
4764
|
+
}, {
|
|
4765
|
+
downloads: 1,
|
|
4766
|
+
applies: 1,
|
|
4767
|
+
failedLaunches: 0
|
|
4768
|
+
}]);
|
|
4769
|
+
});
|
|
4770
|
+
it("counts each launch and download once, so a bundle's launches match its release and its downloads cover launches and crashes", async () => {
|
|
4771
|
+
const model = state.getDatabase();
|
|
4772
|
+
const day = 864e5;
|
|
4773
|
+
const releaseA = "00000000-0000-7000-8000-000000009b01";
|
|
4774
|
+
const releaseB = "00000000-0000-7000-8000-000000009b02";
|
|
4775
|
+
const base = createBundleEventRowFixture("9920", 1e3);
|
|
4776
|
+
const bundleA = base.from_bundle_id;
|
|
4777
|
+
const bundleB = base.to_bundle_id;
|
|
4778
|
+
let sequence = 9920;
|
|
4779
|
+
const report = (install, receivedAtMs, fields) => {
|
|
4780
|
+
sequence += 1;
|
|
4781
|
+
return {
|
|
4782
|
+
...base,
|
|
4783
|
+
id: createBundleEventRowFixture(String(sequence), receivedAtMs).id,
|
|
4784
|
+
install_id: install,
|
|
4785
|
+
from_release_id: releaseA,
|
|
4786
|
+
to_release_id: releaseB,
|
|
4787
|
+
received_at_ms: receivedAtMs,
|
|
4788
|
+
...fields
|
|
4789
|
+
};
|
|
4790
|
+
};
|
|
4791
|
+
const launch = (install, at, bundle) => report(install, at, {
|
|
4792
|
+
type: "UNCHANGED",
|
|
4793
|
+
from_bundle_id: null,
|
|
4794
|
+
from_release_id: null,
|
|
4795
|
+
to_bundle_id: bundle,
|
|
4796
|
+
to_release_id: bundle === bundleA ? releaseA : releaseB,
|
|
4797
|
+
metadata: {
|
|
4798
|
+
...base.metadata,
|
|
4799
|
+
update_strategy: null
|
|
4800
|
+
}
|
|
4801
|
+
});
|
|
4802
|
+
const download = (install, at) => report(install, at, { type: "UPDATE_DOWNLOADED" });
|
|
4803
|
+
const apply = (install, at) => report(install, at, { type: "UPDATE_APPLIED" });
|
|
4804
|
+
const rows = [
|
|
4805
|
+
launch("i1", 1e3, bundleA),
|
|
4806
|
+
download("i1", 2e3),
|
|
4807
|
+
apply("i1", 3e3),
|
|
4808
|
+
launch("i2", 1e3, bundleA),
|
|
4809
|
+
launch("i2", 86401e3, bundleB),
|
|
4810
|
+
launch("i3", 1e3, bundleA),
|
|
4811
|
+
download("i3", 2e3),
|
|
4812
|
+
launch("i3", 86402e3, bundleB),
|
|
4813
|
+
launch("i4", 1e3, bundleA),
|
|
4814
|
+
apply("i4", 3e3),
|
|
4815
|
+
download("i4", 4e3),
|
|
4816
|
+
launch("i5", 1e3, bundleA),
|
|
4817
|
+
download("i5", 2e3),
|
|
4818
|
+
report("i5", 3e3, {
|
|
4819
|
+
type: "RECOVERED",
|
|
4820
|
+
from_release_id: releaseB,
|
|
4821
|
+
from_bundle_id: bundleB,
|
|
4822
|
+
to_release_id: releaseA,
|
|
4823
|
+
to_bundle_id: bundleA
|
|
4824
|
+
})
|
|
4825
|
+
];
|
|
4826
|
+
for (const row of rows) await record(model, row);
|
|
4827
|
+
const scope = {
|
|
4828
|
+
platform: "ios",
|
|
4829
|
+
channel: "production"
|
|
4830
|
+
};
|
|
4831
|
+
const range = {
|
|
4832
|
+
sinceMs: 0,
|
|
4833
|
+
beforeReceivedAtMs: 2 * day
|
|
4834
|
+
};
|
|
4835
|
+
const count = (filter) => model.countEvents({
|
|
4836
|
+
filter,
|
|
4837
|
+
...range
|
|
4838
|
+
});
|
|
4839
|
+
const [{ metrics }] = (await model.getReleaseActivity({ releases: [{
|
|
4840
|
+
releaseId: releaseB,
|
|
4841
|
+
...scope
|
|
4842
|
+
}] })).data;
|
|
4843
|
+
expect(metrics).toEqual({
|
|
4844
|
+
downloads: 5,
|
|
4845
|
+
applies: 4,
|
|
4846
|
+
failedLaunches: 1
|
|
4847
|
+
});
|
|
4848
|
+
await expectInsightsIndex(async () => await count({
|
|
4849
|
+
...scope,
|
|
4850
|
+
type: "UPDATE_APPLIED",
|
|
4851
|
+
toBundleId: bundleB
|
|
4852
|
+
}) + await count({
|
|
4853
|
+
...scope,
|
|
4854
|
+
type: "UNCHANGED",
|
|
4855
|
+
toBundleId: bundleB
|
|
4856
|
+
}), metrics.applies);
|
|
4857
|
+
const implied = (await model.listEvents({
|
|
4858
|
+
filter: { kind: "all" },
|
|
4859
|
+
...range,
|
|
4860
|
+
limit: 100
|
|
4861
|
+
})).filter(({ metadata }) => metadata.implied_download === true).length;
|
|
4862
|
+
await expectInsightsIndex(() => count({
|
|
4863
|
+
...scope,
|
|
4864
|
+
type: "UPDATE_DOWNLOADED",
|
|
4865
|
+
toBundleId: bundleB
|
|
4866
|
+
}), metrics.downloads - implied);
|
|
4867
|
+
expect(implied).toBe(2);
|
|
4868
|
+
expect(metrics.downloads).toBeGreaterThanOrEqual(metrics.applies + metrics.failedLaunches);
|
|
4869
|
+
});
|
|
4646
4870
|
it("lists an update failure where its target's list and installation history read it, and a failed check in no bundle list, moving no latest event", async () => {
|
|
4647
4871
|
const model = state.getDatabase();
|
|
4648
4872
|
const applied = createMovementEvent("721", 100, "UPDATE_APPLIED", "install-failing");
|
|
@@ -4854,10 +5078,11 @@ const setupInsightsHttpTestSuite = (options) => {
|
|
|
4854
5078
|
httpResponse
|
|
4855
5079
|
});
|
|
4856
5080
|
const data = (await (await client.admin(`/installations/${encodeURIComponent(installId)}/events`)).json()).data;
|
|
4857
|
-
expect(data).toHaveLength(
|
|
4858
|
-
|
|
5081
|
+
expect(data).toHaveLength(1);
|
|
5082
|
+
expect(data[0]).toMatchObject({
|
|
4859
5083
|
type,
|
|
4860
|
-
httpResponse
|
|
5084
|
+
httpResponse,
|
|
5085
|
+
...type === "UNCHANGED" ? { change: { kinds: ["first_seen"] } } : {}
|
|
4861
5086
|
});
|
|
4862
5087
|
});
|
|
4863
5088
|
it("records an update failure in installation history and the failures read", async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/test-utils",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.37",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Test utilities for Hot Updater packages",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"mysql2": "*",
|
|
51
51
|
"pg": "*",
|
|
52
52
|
"vitest": "^4.0.0",
|
|
53
|
-
"@hot-updater/plugin-insights": "^1.0.0-rc.
|
|
54
|
-
"@hot-updater/plugin-core": "^1.0.0-rc.
|
|
55
|
-
"@hot-updater/protocol": "^1.0.0-rc.
|
|
56
|
-
"@hot-updater/server": "^1.0.0-rc.
|
|
53
|
+
"@hot-updater/plugin-insights": "^1.0.0-rc.37",
|
|
54
|
+
"@hot-updater/plugin-core": "^1.0.0-rc.37",
|
|
55
|
+
"@hot-updater/protocol": "^1.0.0-rc.37",
|
|
56
|
+
"@hot-updater/server": "^1.0.0-rc.37"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"@electric-sql/pglite": {
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
84
84
|
"vitest": "4.1.4",
|
|
85
85
|
"workspace-tools": "^0.41.7",
|
|
86
|
-
"@hot-updater/plugin-insights": "1.0.0-rc.
|
|
87
|
-
"@hot-updater/
|
|
88
|
-
"@hot-updater/
|
|
89
|
-
"@hot-updater/plugin-core": "1.0.0-rc.
|
|
90
|
-
"@hot-updater/server": "1.0.0-rc.
|
|
86
|
+
"@hot-updater/plugin-insights": "1.0.0-rc.37",
|
|
87
|
+
"@hot-updater/protocol": "1.0.0-rc.37",
|
|
88
|
+
"@hot-updater/js": "1.0.0-rc.37",
|
|
89
|
+
"@hot-updater/plugin-core": "1.0.0-rc.37",
|
|
90
|
+
"@hot-updater/server": "1.0.0-rc.37"
|
|
91
91
|
},
|
|
92
92
|
"inlinedDependencies": {
|
|
93
93
|
"@nodelib/fs.scandir": "2.1.5",
|