@linkforty/core 1.39.0 → 1.41.1

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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/database.d.ts.map +1 -1
  3. package/dist/lib/database.js +5 -0
  4. package/dist/lib/database.js.map +1 -1
  5. package/dist/lib/deep-link-payload.d.ts +6 -0
  6. package/dist/lib/deep-link-payload.d.ts.map +1 -1
  7. package/dist/lib/deep-link-payload.js +6 -0
  8. package/dist/lib/deep-link-payload.js.map +1 -1
  9. package/dist/lib/deep-link-payload.test.js +7 -0
  10. package/dist/lib/deep-link-payload.test.js.map +1 -1
  11. package/dist/lib/fingerprint.d.ts +29 -1
  12. package/dist/lib/fingerprint.d.ts.map +1 -1
  13. package/dist/lib/fingerprint.js +144 -9
  14. package/dist/lib/fingerprint.js.map +1 -1
  15. package/dist/lib/fingerprint.test.js +82 -2
  16. package/dist/lib/fingerprint.test.js.map +1 -1
  17. package/dist/lib/portable/registry.d.ts.map +1 -1
  18. package/dist/lib/portable/registry.js +1 -0
  19. package/dist/lib/portable/registry.js.map +1 -1
  20. package/dist/routes/redirect.d.ts +16 -6
  21. package/dist/routes/redirect.d.ts.map +1 -1
  22. package/dist/routes/redirect.js +44 -9
  23. package/dist/routes/redirect.js.map +1 -1
  24. package/dist/routes/redirect.store-url.test.js +100 -5
  25. package/dist/routes/redirect.store-url.test.js.map +1 -1
  26. package/dist/routes/sdk.d.ts.map +1 -1
  27. package/dist/routes/sdk.install-certainty.integration.test.d.ts +2 -0
  28. package/dist/routes/sdk.install-certainty.integration.test.d.ts.map +1 -0
  29. package/dist/routes/sdk.install-certainty.integration.test.js +104 -0
  30. package/dist/routes/sdk.install-certainty.integration.test.js.map +1 -0
  31. package/dist/routes/sdk.install-referrer.integration.test.d.ts +2 -0
  32. package/dist/routes/sdk.install-referrer.integration.test.d.ts.map +1 -0
  33. package/dist/routes/sdk.install-referrer.integration.test.js +187 -0
  34. package/dist/routes/sdk.install-referrer.integration.test.js.map +1 -0
  35. package/dist/routes/sdk.js +19 -2
  36. package/dist/routes/sdk.js.map +1 -1
  37. package/llms.txt +4 -0
  38. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Store URLs leave the redirect exactly as configured.
2
+ * Store URLs are not decorated like websites.
3
3
  *
4
4
  * The destination decorator adds UTM tags, deep-link parameters and the
5
5
  * opt-in click id — right for a website, wrong for a store listing. Google
6
6
  * Play identifies the app by `?id=`, so a deep-link parameter named `id`
7
7
  * replaced it and Play answered 404 to every Android visitor sent to install.
8
+ * The one thing a Play listing gains, for an Android visitor, is the click id
9
+ * in its `referrer`, which Play hands to the installed app.
8
10
  *
9
11
  * The app-open guard cases matter as much as the fix: an in-app browser must
10
12
  * still be sent to the web fallback (where a Universal Link / App Link gets a
@@ -15,7 +17,7 @@
15
17
  */
16
18
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
17
19
  import Fastify from 'fastify';
18
- import { redirectRoutes, isAppStoreUrl } from './redirect.js';
20
+ import { redirectRoutes, isAppStoreUrl, withPlayInstallReferrer } from './redirect.js';
19
21
  const query = vi.fn();
20
22
  vi.mock('../lib/database.js', () => ({
21
23
  db: {
@@ -82,12 +84,31 @@ afterEach(async () => {
82
84
  await new Promise((r) => setImmediate(r));
83
85
  await app.close();
84
86
  });
87
+ /** The click id the redirect wrote to click_events. */
88
+ function insertedClickId() {
89
+ const insert = query.mock.calls.find(([sql]) => /INSERT INTO click_events/i.test(String(sql)));
90
+ return insert?.[1]?.[0];
91
+ }
92
+ /** `location` as a Play URL: its app id, and its referrer's keys. */
93
+ function playParts(location) {
94
+ const url = new URL(String(location));
95
+ return {
96
+ base: `${url.origin}${url.pathname}`,
97
+ keys: [...url.searchParams.keys()].sort(),
98
+ id: url.searchParams.get('id'),
99
+ referrer: new URLSearchParams(url.searchParams.get('referrer') ?? ''),
100
+ };
101
+ }
85
102
  describe('store URLs are sent exactly as configured', () => {
86
- it('Android Chrome → the Play URL untouched, so a deep-link `id` cannot replace the app id', async () => {
103
+ it('Android Chrome → the Play URL with only the click id added, so a deep-link `id` cannot replace the app id', async () => {
87
104
  mockDb(linkRow({ web_fallback_url: WEB }));
88
105
  const res = await get(app, ANDROID_CHROME_UA);
89
106
  expect(res.statusCode).toBe(302);
90
- expect(res.headers.location).toBe(PLAY);
107
+ const play = playParts(res.headers.location);
108
+ expect(play.base).toBe('https://play.google.com/store/apps/details');
109
+ expect(play.keys).toEqual(['id', 'referrer']);
110
+ expect(play.id).toBe('com.example.app');
111
+ expect([...play.referrer.keys()]).toEqual(['lf_click']);
91
112
  });
92
113
  it('iOS Safari → the App Store URL untouched', async () => {
93
114
  mockDb(linkRow({ web_fallback_url: WEB }));
@@ -96,9 +117,83 @@ describe('store URLs are sent exactly as configured', () => {
96
117
  expect(res.headers.location).toBe(APP_STORE);
97
118
  });
98
119
  it('a store URL configured as the web fallback is not decorated either', async () => {
120
+ mockDb(linkRow({ ios_app_store_url: null, android_app_store_url: null, web_fallback_url: PLAY }));
121
+ const res = await get(app, IPHONE_UA);
122
+ expect(res.headers.location).toBe(PLAY);
123
+ });
124
+ });
125
+ describe('the Play install referrer carries the click id', () => {
126
+ it('is the id of the click the redirect records', async () => {
127
+ mockDb(linkRow());
128
+ const res = await get(app, ANDROID_CHROME_UA);
129
+ await new Promise((r) => setImmediate(r));
130
+ const clickId = playParts(res.headers.location).referrer.get('lf_click');
131
+ expect(clickId).toMatch(/^[0-9a-f-]{36}$/);
132
+ expect(insertedClickId()).toBe(clickId);
133
+ });
134
+ it('is added to a Play URL configured as the web fallback', async () => {
99
135
  mockDb(linkRow({ android_app_store_url: null, web_fallback_url: PLAY }));
100
136
  const res = await get(app, ANDROID_CHROME_UA);
101
- expect(res.headers.location).toBe(PLAY);
137
+ const play = playParts(res.headers.location);
138
+ expect(play.id).toBe('com.example.app');
139
+ expect(play.referrer.get('lf_click')).toBeTruthy();
140
+ });
141
+ it('keeps the keys of a referrer the Play URL already carries', async () => {
142
+ mockDb(linkRow({ android_app_store_url: `${PLAY}&referrer=utm_source%3Dposter` }));
143
+ const res = await get(app, ANDROID_CHROME_UA);
144
+ const { referrer } = playParts(res.headers.location);
145
+ expect(referrer.get('utm_source')).toBe('poster');
146
+ expect(referrer.get('lf_click')).toBeTruthy();
147
+ });
148
+ it('is the scheme interstitial\'s store fallback on Android', async () => {
149
+ mockDb(linkRow({ app_scheme: 'example', deep_link_path: '/e/42' }));
150
+ const res = await get(app, ANDROID_CHROME_UA);
151
+ expect(res.statusCode).toBe(200);
152
+ const fallback = /id="store-btn" href="([^"]+)"/.exec(res.body)?.[1].replace(/&/g, '&');
153
+ expect(playParts(fallback).referrer.get('lf_click')).toBeTruthy();
154
+ });
155
+ it('is the launchpad page\'s Google Play button on Android', async () => {
156
+ mockDb(linkRow({ org_settings: { launchpad: { mobile: 'page' } } }));
157
+ const res = await get(app, ANDROID_CHROME_UA);
158
+ expect(res.statusCode).toBe(200);
159
+ const button = /data-lp-cta="cta_android" href="([^"]+)"/.exec(res.body)?.[1].replace(/&/g, '&');
160
+ expect(playParts(button).referrer.get('lf_click')).toBeTruthy();
161
+ });
162
+ it('is not added for an iPhone or a desktop visitor', async () => {
163
+ mockDb(linkRow({ ios_app_store_url: null, android_app_store_url: null, web_fallback_url: PLAY }));
164
+ expect((await get(app, IPHONE_UA)).headers.location).toBe(PLAY);
165
+ expect((await get(app, DESKTOP_UA)).headers.location).toBe(PLAY);
166
+ });
167
+ it('app-open guard: an Android in-app browser still goes to the web fallback, with no referrer', async () => {
168
+ mockDb(linkRow({ web_fallback_url: WEB }));
169
+ const res = await get(app, ANDROID_INSTAGRAM_UA);
170
+ const url = new URL(String(res.headers.location));
171
+ expect(url.hostname).toBe('example.com');
172
+ expect(url.searchParams.has('referrer')).toBe(false);
173
+ });
174
+ });
175
+ describe('withPlayInstallReferrer', () => {
176
+ const CLICK = '11111111-2222-4333-8444-555555555555';
177
+ it('adds the click id to a Play listing, https or market://', () => {
178
+ expect(withPlayInstallReferrer(PLAY, CLICK)).toBe(`${PLAY}&referrer=lf_click%3D${CLICK}`);
179
+ expect(withPlayInstallReferrer('market://details?id=com.example.app', CLICK)).toBe(`market://details?id=com.example.app&referrer=lf_click%3D${CLICK}`);
180
+ });
181
+ it('replaces a click id the referrer already names', () => {
182
+ const url = withPlayInstallReferrer(`${PLAY}&referrer=lf_click%3Dold%26utm_source%3Dx`, CLICK);
183
+ const referrer = new URLSearchParams(new URL(url).searchParams.get('referrer') ?? '');
184
+ expect(referrer.getAll('lf_click')).toEqual([CLICK]);
185
+ expect(referrer.get('utm_source')).toBe('x');
186
+ });
187
+ it('leaves every other URL alone', () => {
188
+ for (const url of [
189
+ APP_STORE,
190
+ WEB,
191
+ 'https://play.google.com/store/apps/dev?id=123',
192
+ 'https://play.google.com.evil.example/store/apps/details?id=x',
193
+ 'not a url',
194
+ ]) {
195
+ expect(withPlayInstallReferrer(url, CLICK)).toBe(url);
196
+ }
102
197
  });
103
198
  });
104
199
  describe('websites are decorated exactly as before', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"redirect.store-url.test.js","sourceRoot":"","sources":["../../src/routes/redirect.store-url.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"redirect.store-url.test.js","sourceRoot":"","sources":["../../src/routes/redirect.store-url.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/routes/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK1D,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAmE/B,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KACxB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,iBAAiB,GAAG,IAAI,CAAC;CACnE;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE,eAAoB,iBAiwBtF"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/routes/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK1D,OAAO,EAKL,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAmE/B,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KACxB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,iBAAiB,GAAG,IAAI,CAAC;CACnE;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,GAAE,eAAoB,iBAmxBtF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sdk.install-certainty.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.install-certainty.integration.test.d.ts","sourceRoot":"","sources":["../../src/routes/sdk.install-certainty.integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,104 @@
1
+ import pg from 'pg';
2
+ import Fastify from 'fastify';
3
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
4
+ import { db, initializeDatabase } from '../lib/database.js';
5
+ import { linkRoutes } from './links.js';
6
+ import { sdkRoutes } from './sdk.js';
7
+ /**
8
+ * The install response says how the install was matched and how many other
9
+ * links were in contention, and the install row records the same.
10
+ *
11
+ * Each case clicks from its own /24, so it can only match its own link.
12
+ * Needs a real database; skipped without DATABASE_URL, like database.test.ts.
13
+ */
14
+ const ADMIN_URL = process.env.DATABASE_URL;
15
+ const clickUserAgent = 'Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 Chrome/130.0 Mobile Safari/537.36';
16
+ const installBody = {
17
+ userAgent: 'App/1.0 Android/15',
18
+ timezone: 'Europe/Madrid',
19
+ language: 'en-ES',
20
+ screenWidth: 1080,
21
+ screenHeight: 2400,
22
+ platform: 'android',
23
+ platformVersion: '15',
24
+ };
25
+ describe.skipIf(!ADMIN_URL)('install match certainty (integration)', () => {
26
+ const dbName = `lf_core_certainty_${Date.now()}`;
27
+ let admin;
28
+ let app;
29
+ beforeAll(async () => {
30
+ admin = new pg.Client({ connectionString: ADMIN_URL });
31
+ await admin.connect();
32
+ await admin.query(`CREATE DATABASE ${dbName}`);
33
+ const url = new URL(ADMIN_URL);
34
+ url.pathname = `/${dbName}`;
35
+ await initializeDatabase({ url: url.toString(), pool: { min: 1, max: 3 } });
36
+ app = Fastify();
37
+ await app.register(linkRoutes);
38
+ await app.register(sdkRoutes);
39
+ await app.ready();
40
+ }, 120000);
41
+ afterAll(async () => {
42
+ await app?.close();
43
+ await db.end().catch(() => undefined);
44
+ await admin.query(`DROP DATABASE IF EXISTS ${dbName} WITH (FORCE)`);
45
+ await admin.end();
46
+ }, 60000);
47
+ async function createLink(body) {
48
+ const res = await app.inject({
49
+ method: 'POST',
50
+ url: '/api/links',
51
+ payload: { originalUrl: 'https://example.com/landing', ...body },
52
+ });
53
+ expect(res.statusCode).toBe(200);
54
+ return res.json();
55
+ }
56
+ /** A click on the link from `network`.1, `minutesAgo` minutes ago by the database clock. */
57
+ async function click(linkId, network, minutesAgo) {
58
+ const ce = await db.query(`INSERT INTO click_events (link_id, clicked_at, ip_address, user_agent, platform)
59
+ VALUES ($1, NOW() - make_interval(mins => $2), $3, $4, 'android') RETURNING id`, [linkId, minutesAgo, `${network}.1`, clickUserAgent]);
60
+ await db.query(`INSERT INTO device_fingerprints
61
+ (click_id, fingerprint_hash, ip_address, user_agent, timezone, language, screen_width, screen_height, platform, platform_version)
62
+ VALUES ($1, 'h', $2, $3, 'Europe/Madrid', 'en-ES', 412, 915, 'android', '15')`, [ce.rows[0].id, `${network}.1`, clickUserAgent]);
63
+ return ce.rows[0].id;
64
+ }
65
+ async function installFrom(network, host = 2) {
66
+ const res = await app.inject({
67
+ method: 'POST',
68
+ url: '/api/sdk/v1/install',
69
+ payload: installBody,
70
+ remoteAddress: `${network}.${host}`,
71
+ });
72
+ expect(res.statusCode).toBe(200);
73
+ return res.json();
74
+ }
75
+ async function storedCompetingLinks(installId) {
76
+ const row = await db.query('SELECT attribution_method, competing_links FROM install_events WHERE id = $1', [installId]);
77
+ return row.rows[0];
78
+ }
79
+ it('reports no competing links when one link was clicked', async () => {
80
+ const link = await createLink({});
81
+ await click(link.id, '81.2.90', 5);
82
+ const body = await installFrom('81.2.90');
83
+ expect(body).toMatchObject({ attributed: true, attributionMethod: 'fingerprint', competingLinks: 0 });
84
+ expect(body.deepLinkData).toMatchObject({ attributionMethod: 'fingerprint', competingLinks: 0 });
85
+ expect(await storedCompetingLinks(body.installId)).toEqual({ attribution_method: 'fingerprint', competing_links: 0 });
86
+ });
87
+ it('counts another link clicked from the same network by a matching device', async () => {
88
+ const mine = await createLink({});
89
+ const theirs = await createLink({});
90
+ await click(mine.id, '81.2.91', 6);
91
+ await click(theirs.id, '81.2.91', 4);
92
+ await click(theirs.id, '81.2.91', 3);
93
+ const body = await installFrom('81.2.91');
94
+ expect(body).toMatchObject({ attributed: true, competingLinks: 1 });
95
+ expect(body.deepLinkData.competingLinks).toBe(1);
96
+ expect((await storedCompetingLinks(body.installId)).competing_links).toBe(1);
97
+ });
98
+ it('reports an organic install as none', async () => {
99
+ const body = await installFrom('81.2.92');
100
+ expect(body).toMatchObject({ attributed: false, attributionMethod: 'none', competingLinks: 0, deepLinkData: null });
101
+ expect(await storedCompetingLinks(body.installId)).toEqual({ attribution_method: 'none', competing_links: null });
102
+ });
103
+ });
104
+ //# sourceMappingURL=sdk.install-certainty.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.install-certainty.integration.test.js","sourceRoot":"","sources":["../../src/routes/sdk.install-certainty.integration.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sdk.install-referrer.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.install-referrer.integration.test.d.ts","sourceRoot":"","sources":["../../src/routes/sdk.install-referrer.integration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,187 @@
1
+ import pg from 'pg';
2
+ import Fastify from 'fastify';
3
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
4
+ import { db, initializeDatabase } from '../lib/database.js';
5
+ import { linkRoutes } from './links.js';
6
+ import { redirectRoutes } from './redirect.js';
7
+ import { sdkRoutes } from './sdk.js';
8
+ /**
9
+ * An install that sends the Google Play install referrer is matched to the
10
+ * click the referrer names, under the same rules as a fingerprint match, and
11
+ * a referrer without a click id leaves the install to its fingerprint.
12
+ *
13
+ * Each case uses its own /24, so a fingerprint can only match that case's
14
+ * clicks. Needs a real database; skipped without DATABASE_URL, like
15
+ * database.test.ts.
16
+ */
17
+ const ADMIN_URL = process.env.DATABASE_URL;
18
+ const clickUserAgent = 'Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 Chrome/130.0 Mobile Safari/537.36';
19
+ const installBody = {
20
+ userAgent: 'App/1.0 Android/15',
21
+ timezone: 'Europe/Madrid',
22
+ language: 'en-ES',
23
+ screenWidth: 1080,
24
+ screenHeight: 2400,
25
+ platform: 'android',
26
+ platformVersion: '15',
27
+ };
28
+ const UNKNOWN_CLICK = '00000000-0000-4000-8000-000000000000';
29
+ describe.skipIf(!ADMIN_URL)('install referrer matching (integration)', () => {
30
+ const dbName = `lf_core_referrer_${Date.now()}`;
31
+ let admin;
32
+ let app;
33
+ let scopedApp;
34
+ beforeAll(async () => {
35
+ admin = new pg.Client({ connectionString: ADMIN_URL });
36
+ await admin.connect();
37
+ await admin.query(`CREATE DATABASE ${dbName}`);
38
+ const url = new URL(ADMIN_URL);
39
+ url.pathname = `/${dbName}`;
40
+ await initializeDatabase({ url: url.toString(), pool: { min: 1, max: 3 } });
41
+ app = Fastify();
42
+ await app.register(linkRoutes);
43
+ await app.register(sdkRoutes);
44
+ await app.register(redirectRoutes);
45
+ await app.ready();
46
+ // A deployment serving several apps: the app token names the organization.
47
+ scopedApp = Fastify();
48
+ const scoped = {
49
+ resolveInstallScope: (_request, { appToken }) => (appToken ? { organizationId: appToken } : { matchNothing: true }),
50
+ };
51
+ await scopedApp.register(sdkRoutes, scoped);
52
+ await scopedApp.ready();
53
+ }, 120000);
54
+ afterAll(async () => {
55
+ await app?.close();
56
+ await scopedApp?.close();
57
+ await db.end().catch(() => undefined);
58
+ await admin.query(`DROP DATABASE IF EXISTS ${dbName} WITH (FORCE)`);
59
+ await admin.end();
60
+ }, 60000);
61
+ async function createLink(body = {}) {
62
+ const res = await app.inject({
63
+ method: 'POST',
64
+ url: '/api/links',
65
+ payload: { originalUrl: 'https://example.com/landing', ...body },
66
+ });
67
+ expect(res.statusCode).toBe(200);
68
+ return res.json();
69
+ }
70
+ /** A click on the link from `network`.1, `minutesAgo` minutes ago by the database clock. */
71
+ async function click(linkId, network, minutesAgo) {
72
+ const ce = await db.query(`INSERT INTO click_events (link_id, clicked_at, ip_address, user_agent, platform)
73
+ VALUES ($1, NOW() - make_interval(mins => $2), $3, $4, 'android') RETURNING id`, [linkId, minutesAgo, `${network}.1`, clickUserAgent]);
74
+ await db.query(`INSERT INTO device_fingerprints
75
+ (click_id, fingerprint_hash, ip_address, user_agent, timezone, language, screen_width, screen_height, platform, platform_version)
76
+ VALUES ($1, 'h', $2, $3, 'Europe/Madrid', 'en-ES', 412, 915, 'android', '15')`, [ce.rows[0].id, `${network}.1`, clickUserAgent]);
77
+ return ce.rows[0].id;
78
+ }
79
+ async function install(network, extra = {}, target = app) {
80
+ const res = await target.inject({
81
+ method: 'POST',
82
+ url: '/api/sdk/v1/install',
83
+ payload: { ...installBody, ...extra },
84
+ remoteAddress: `${network}.2`,
85
+ });
86
+ expect(res.statusCode).toBe(200);
87
+ return res.json();
88
+ }
89
+ async function stored(installId) {
90
+ const row = await db.query('SELECT link_id, click_id, attribution_method, confidence_score, matched_factors, competing_links FROM install_events WHERE id = $1', [installId]);
91
+ return row.rows[0];
92
+ }
93
+ it('matches the click the Play redirect named, end to end', async () => {
94
+ const link = await createLink({ androidAppStoreUrl: 'https://play.google.com/store/apps/details?id=com.example.app' });
95
+ const redirect = await app.inject({
96
+ method: 'GET',
97
+ url: `/${link.short_code}`,
98
+ headers: { 'user-agent': clickUserAgent },
99
+ remoteAddress: '81.3.10.1',
100
+ });
101
+ expect(redirect.statusCode).toBe(302);
102
+ const referrer = new URL(String(redirect.headers.location)).searchParams.get('referrer');
103
+ expect(referrer).toMatch(/^lf_click=[0-9a-f-]{36}$/);
104
+ const clickId = new URLSearchParams(referrer ?? '').get('lf_click');
105
+ // The click is written after the response.
106
+ for (let i = 0; i < 50; i++) {
107
+ const found = await db.query('SELECT 1 FROM click_events WHERE id = $1', [clickId]);
108
+ if (found.rowCount)
109
+ break;
110
+ await new Promise((r) => setTimeout(r, 20));
111
+ }
112
+ // Installed on another network (mobile data, say): no fingerprint could match.
113
+ const body = await install('81.3.11', { installReferrer: `${referrer}&utm_source=google-play` });
114
+ expect(body).toMatchObject({
115
+ attributed: true,
116
+ confidenceScore: 100,
117
+ matchedFactors: ['install_referrer'],
118
+ attributionMethod: 'install_referrer',
119
+ competingLinks: 0,
120
+ });
121
+ expect(body.deepLinkData).toMatchObject({ shortCode: link.short_code, attributionMethod: 'install_referrer', competingLinks: 0 });
122
+ expect(await stored(body.installId)).toMatchObject({
123
+ link_id: link.id,
124
+ click_id: clickId,
125
+ attribution_method: 'install_referrer',
126
+ matched_factors: ['install_referrer'],
127
+ competing_links: 0,
128
+ });
129
+ });
130
+ it('prefers the named click over a fingerprint match on the install network', async () => {
131
+ const named = await createLink();
132
+ const nearby = await createLink();
133
+ const clickId = await click(named.id, '81.3.20', 30);
134
+ await click(nearby.id, '81.3.21', 1);
135
+ const body = await install('81.3.21', { installReferrer: `lf_click=${clickId}` });
136
+ expect(body.attributionMethod).toBe('install_referrer');
137
+ expect((await stored(body.installId)).link_id).toBe(named.id);
138
+ });
139
+ it('gives no match, not a fingerprint guess, when the named click is outside its window', async () => {
140
+ const named = await createLink({ attributionWindowMinutes: 10 });
141
+ const nearby = await createLink();
142
+ const clickId = await click(named.id, '81.3.30', 20);
143
+ await click(nearby.id, '81.3.31', 1);
144
+ const body = await install('81.3.31', { installReferrer: `lf_click=${clickId}` });
145
+ expect(body).toMatchObject({ attributed: false, attributionMethod: 'none', deepLinkData: null });
146
+ });
147
+ it('gives no match when the named click is on a link that no longer opens', async () => {
148
+ const named = await createLink();
149
+ const clickId = await click(named.id, '81.3.40', 5);
150
+ await db.query('UPDATE links SET is_active = false WHERE id = $1', [named.id]);
151
+ const body = await install('81.3.40', { installReferrer: `lf_click=${clickId}` });
152
+ expect(body).toMatchObject({ attributed: false, attributionMethod: 'none' });
153
+ });
154
+ it('hands a one-install click to the first install only', async () => {
155
+ const named = await createLink({ oneInstallPerClick: true });
156
+ const clickId = await click(named.id, '81.3.50', 5);
157
+ const first = await install('81.3.51', { installReferrer: `lf_click=${clickId}` });
158
+ expect(first.attributionMethod).toBe('install_referrer');
159
+ const second = await install('81.3.52', { installReferrer: `lf_click=${clickId}` });
160
+ expect(second).toMatchObject({ attributed: false, attributionMethod: 'none' });
161
+ });
162
+ it('falls back to the fingerprint for a referrer with no click id, an unknown one, or a non-string', async () => {
163
+ const link = await createLink();
164
+ await click(link.id, '81.3.60', 5);
165
+ for (const installReferrer of ['utm_source=google-play&utm_medium=organic', `lf_click=${UNKNOWN_CLICK}`, 42]) {
166
+ const body = await install('81.3.60', { installReferrer });
167
+ expect(body).toMatchObject({ attributed: true, attributionMethod: 'fingerprint' });
168
+ }
169
+ });
170
+ it('matches only clicks in the installing app\'s scope', async () => {
171
+ const org = await db.query(`INSERT INTO organizations (name) VALUES ('Referrer test') RETURNING id`);
172
+ const otherOrg = await db.query(`INSERT INTO organizations (name) VALUES ('Referrer test, other') RETURNING id`);
173
+ const mine = await createLink();
174
+ const theirs = await createLink();
175
+ await db.query('UPDATE links SET organization_id = $1 WHERE id = $2', [org.rows[0].id, mine.id]);
176
+ await db.query('UPDATE links SET organization_id = $1 WHERE id = $2', [otherOrg.rows[0].id, theirs.id]);
177
+ const myClick = await click(mine.id, '81.3.70', 5);
178
+ const theirClick = await click(theirs.id, '81.3.71', 5);
179
+ const matched = await install('81.3.72', { installReferrer: `lf_click=${myClick}`, appToken: org.rows[0].id }, scopedApp);
180
+ expect(matched.attributionMethod).toBe('install_referrer');
181
+ const refused = await install('81.3.73', { installReferrer: `lf_click=${theirClick}`, appToken: org.rows[0].id }, scopedApp);
182
+ expect(refused).toMatchObject({ attributed: false, attributionMethod: 'none' });
183
+ const tokenless = await install('81.3.74', { installReferrer: `lf_click=${myClick}` }, scopedApp);
184
+ expect(tokenless).toMatchObject({ attributed: false, attributionMethod: 'none' });
185
+ });
186
+ });
187
+ //# sourceMappingURL=sdk.install-referrer.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.install-referrer.integration.test.js","sourceRoot":"","sources":["../../src/routes/sdk.install-referrer.integration.test.ts"],"names":[],"mappings":"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"}
@@ -76,13 +76,25 @@ export async function sdkRoutes(fastify, options = {}) {
76
76
  * - platformVersion: Platform version (e.g., "15.0")
77
77
  * - deviceId: Optional device identifier (IDFA, GAID, etc.)
78
78
  * - attributionWindowHours: Optional custom attribution window (default: 168 = 7 days)
79
+ * - installReferrer: Optional, Android: the Google Play install referrer, as
80
+ * the Play Install Referrer API returns it. The redirect puts the click id
81
+ * in every Play listing's referrer (`lf_click=<id>`), so when it is there
82
+ * the install is matched to that exact click instead of by fingerprint.
79
83
  *
80
84
  * Response:
81
85
  * - installId: UUID of the install event
82
86
  * - attributed: Boolean indicating if install was matched to a click
83
87
  * - confidenceScore: Confidence score (0-100) if matched
84
88
  * - matchedFactors: Array of matched fingerprint factors
85
- * - deepLinkData: Deep link data if matched (shortCode, URLs, UTM params, etc.)
89
+ * - attributionMethod: How the install was matched: 'install_referrer' (the
90
+ * click named by the Play install referrer), 'fingerprint', or 'none' (organic)
91
+ * - competingLinks: How many other links also had a click from the install's
92
+ * network that reached the threshold (always 0 for 'install_referrer').
93
+ * 0 when nothing else was in contention; above 0, another device's tap
94
+ * may have led to this install. An app can
95
+ * treat that as unverified, e.g. confirm before acting on a sign-in link.
96
+ * - deepLinkData: Deep link data if matched (shortCode, URLs, UTM params,
97
+ * confidenceScore, matchedFactors, attributionMethod, competingLinks, etc.)
86
98
  */
87
99
  fastify.post('/api/sdk/v1/install', async (request, reply) => {
88
100
  const schema = z.object({
@@ -106,6 +118,9 @@ export async function sdkRoutes(fastify, options = {}) {
106
118
  // host reads it to route the install to the correct tenant; self-hosted
107
119
  // single-tenant deployments simply ignore it.
108
120
  appToken: z.string().optional(),
121
+ // Read leniently: a referrer this server cannot use is ignored, never
122
+ // a reason to reject the install.
123
+ installReferrer: z.unknown().optional(),
109
124
  });
110
125
  const body = schema.parse(request.body);
111
126
  // Trusted IP from connection/proxy headers only; never use body.ipAddress for attribution/fingerprint
@@ -133,12 +148,14 @@ export async function sdkRoutes(fastify, options = {}) {
133
148
  }
134
149
  }
135
150
  try {
136
- const result = await recordInstallEvent(fingerprintData, body.deviceId, body.attributionWindowHours, { name: body.sdkName, version: body.sdkVersion }, scope);
151
+ const result = await recordInstallEvent(fingerprintData, body.deviceId, body.attributionWindowHours, { name: body.sdkName, version: body.sdkVersion }, scope, typeof body.installReferrer === 'string' ? body.installReferrer : undefined);
137
152
  return reply.status(200).send({
138
153
  installId: result.installId,
139
154
  attributed: result.match !== null,
140
155
  confidenceScore: result.match?.confidenceScore || 0,
141
156
  matchedFactors: result.match?.matchedFactors || [],
157
+ attributionMethod: result.attributionMethod,
158
+ competingLinks: result.match?.competingLinks ?? 0,
142
159
  deepLinkData: result.deepLinkData,
143
160
  ...(body.ipAddress != null && { clientReportedIp: body.ipAddress }),
144
161
  });