@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.
Files changed (2) hide show
  1. package/dist/index.mjs +246 -21
  2. 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 25 Insights
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 before T0, as a production database
2908
- * holds, so a read that scans beyond its window examines more rows.
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(26, {
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(4, 6, 0, 0),
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
- await expect(model.findLatestEvents({ installId: unchanged.install_id })).resolves.toEqual([unchanged]);
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 expect(model.countEvents({
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
- })).rejects.toThrow();
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.from_bundle_id
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
- ...createBundleEventRowFixture("711", 300),
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
- ...createBundleEventRowFixture("711", 300).metadata,
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
- unchanged,
4650
+ recovered,
4621
4651
  unrelated,
4622
4652
  applied,
4623
- recovered
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(type === "UNCHANGED" ? 0 : 1);
4858
- if (type !== "UNCHANGED") expect(data[0]).toMatchObject({
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.36",
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.36",
54
- "@hot-updater/plugin-core": "^1.0.0-rc.36",
55
- "@hot-updater/protocol": "^1.0.0-rc.36",
56
- "@hot-updater/server": "^1.0.0-rc.36"
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.36",
87
- "@hot-updater/js": "1.0.0-rc.36",
88
- "@hot-updater/protocol": "1.0.0-rc.36",
89
- "@hot-updater/plugin-core": "1.0.0-rc.36",
90
- "@hot-updater/server": "1.0.0-rc.36"
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",