@linkforty/core 1.31.0 → 1.33.0
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/README.md +37 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts +2 -0
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +92 -2
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/fingerprint.d.ts +28 -2
- package/dist/lib/fingerprint.d.ts.map +1 -1
- package/dist/lib/fingerprint.js +109 -43
- package/dist/lib/fingerprint.js.map +1 -1
- package/dist/lib/fingerprint.test.js +77 -0
- package/dist/lib/fingerprint.test.js.map +1 -1
- package/dist/lib/link-lookup.d.ts +50 -0
- package/dist/lib/link-lookup.d.ts.map +1 -0
- package/dist/lib/link-lookup.js +54 -0
- package/dist/lib/link-lookup.js.map +1 -0
- package/dist/lib/link-lookup.test.d.ts +2 -0
- package/dist/lib/link-lookup.test.d.ts.map +1 -0
- package/dist/lib/link-lookup.test.js +53 -0
- package/dist/lib/link-lookup.test.js.map +1 -0
- package/dist/lib/link-resolution-cache.d.ts +3 -4
- package/dist/lib/link-resolution-cache.d.ts.map +1 -1
- package/dist/lib/link-resolution-cache.js +8 -6
- package/dist/lib/link-resolution-cache.js.map +1 -1
- package/dist/lib/link-resolution-cache.test.js +5 -0
- package/dist/lib/link-resolution-cache.test.js.map +1 -1
- package/dist/lib/webhook.d.ts +22 -1
- package/dist/lib/webhook.d.ts.map +1 -1
- package/dist/lib/webhook.js +32 -2
- package/dist/lib/webhook.js.map +1 -1
- package/dist/lib/webhook.test.js +55 -4
- package/dist/lib/webhook.test.js.map +1 -1
- package/dist/routes/links.js +5 -5
- package/dist/routes/links.js.map +1 -1
- package/dist/routes/preview.d.ts.map +1 -1
- package/dist/routes/preview.js +31 -13
- package/dist/routes/preview.js.map +1 -1
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +16 -10
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/sdk.d.ts +15 -2
- package/dist/routes/sdk.d.ts.map +1 -1
- package/dist/routes/sdk.js +30 -18
- package/dist/routes/sdk.js.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -296,6 +296,8 @@ POST /api/webhooks/:id/test?userId=user-uuid
|
|
|
296
296
|
|
|
297
297
|
Events: `click_event`, `install_event`, `conversion_event`, `sdk_event`. Payloads are HMAC SHA-256 signed.
|
|
298
298
|
|
|
299
|
+
Which webhooks hear about a link: one with an `organization_id` receives events for every link in that organization; one without receives events for links created by its `user_id`; one with neither receives events for links that have neither. To keep a delivery log, pass a function to `setWebhookDeliveryLogger()`. It is called with the webhook id, the attempt's result and the payload sent.
|
|
300
|
+
|
|
299
301
|
### Mobile SDK Endpoints
|
|
300
302
|
|
|
301
303
|
```bash
|
|
@@ -371,6 +373,7 @@ interface ServerOptions {
|
|
|
371
373
|
logger?: boolean; // Enable Fastify logger (default: true)
|
|
372
374
|
trustProxy?: boolean | number; // Trust X-Forwarded-For when behind a proxy (default: false)
|
|
373
375
|
redirect?: RedirectRouteOptions; // See below
|
|
376
|
+
sdk?: SdkRouteOptions; // See below
|
|
374
377
|
}
|
|
375
378
|
|
|
376
379
|
interface RedirectRouteOptions {
|
|
@@ -398,8 +401,21 @@ interface RedirectRouteOptions {
|
|
|
398
401
|
// A hook that throws is logged and treated as false.
|
|
399
402
|
isWebLinkRequest?: (request) => Promise<boolean> | boolean;
|
|
400
403
|
}
|
|
404
|
+
|
|
405
|
+
interface SdkRouteOptions {
|
|
406
|
+
// Narrow the clicks an install can be attributed to. A server shared by
|
|
407
|
+
// several apps resolves the installing app (from the SDK's `appToken`, say)
|
|
408
|
+
// and returns its organization, so an install only ever matches — and
|
|
409
|
+
// receives the deep link of — that organization's links. Return null to
|
|
410
|
+
// match against every click, as when the option is absent. A hook that
|
|
411
|
+
// throws is logged and treated as null.
|
|
412
|
+
resolveInstallScope?: (request, body: { appToken?: string }) =>
|
|
413
|
+
Promise<{ organizationId?: string } | null> | { organizationId?: string } | null;
|
|
414
|
+
}
|
|
401
415
|
```
|
|
402
416
|
|
|
417
|
+
Install matching only considers clicks from the installing device's own network (its IPv4 /24 or IPv6 /64), since no other click can reach the confidence threshold, so the attribution window holds however much traffic the server takes. It relies on the `idx_fingerprints_ip` index, which `initializeDatabase()` builds concurrently on first start so click writes are never blocked.
|
|
418
|
+
|
|
403
419
|
`renderLaunchpadPage()`, `defaultLaunchpadContent()` and `shouldServeLaunchpadOnDesktop()` are exported so a host can render the same page elsewhere (a settings preview, for instance).
|
|
404
420
|
|
|
405
421
|
### Running behind a reverse proxy
|
|
@@ -431,13 +447,32 @@ SHORTLINK_DOMAIN=yourdomain.com
|
|
|
431
447
|
|
|
432
448
|
Core does not create a `users` table. Authentication and user management are the consumer's responsibility. The `user_id` column on `links` and `webhooks` is optional (nullable, no foreign key) — use it for multi-tenant scoping when your auth layer provides a user identity.
|
|
433
449
|
|
|
450
|
+
### Short codes and URL resolution
|
|
451
|
+
|
|
452
|
+
Every short code is unique across the server. Links created through Core's API have no organization, so they all share one set of codes.
|
|
453
|
+
|
|
454
|
+
An application that builds on Core and assigns links to organizations (by setting `links.organization_id`) gets one set of codes per organization: two organizations can each have `promo`. Template slugs stay unique across the server, and URLs resolve like this:
|
|
455
|
+
|
|
456
|
+
- `/{templateSlug}/{shortCode}` resolves to exactly one link.
|
|
457
|
+
- `/{shortCode}` prefers the organization the request belongs to, then a link without a template, then the oldest link with that code.
|
|
458
|
+
|
|
459
|
+
To tell Core which organization a request belongs to, for example by custom domain, register a resolver:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { setLinkScopeResolver } from '@linkforty/core';
|
|
463
|
+
|
|
464
|
+
setLinkScopeResolver(async (request) => organizationIdForHost(request.headers.host) ?? null);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
The redirect, SDK resolve and preview routes all use it. A resolver that throws is logged and treated as null.
|
|
468
|
+
|
|
434
469
|
### Links Table
|
|
435
470
|
|
|
436
471
|
| Column | Type | Description |
|
|
437
472
|
|-------------------------|--------------|------------------------------------------|
|
|
438
473
|
| id | UUID | Primary key |
|
|
439
474
|
| user_id | UUID | Optional owner/tenant identifier |
|
|
440
|
-
| short_code | VARCHAR(20) |
|
|
475
|
+
| short_code | VARCHAR(20) | Short code, unique per organization_id |
|
|
441
476
|
| original_url | TEXT | Original URL |
|
|
442
477
|
| title | VARCHAR(255) | Link title |
|
|
443
478
|
| description | TEXT | Link description |
|
|
@@ -536,6 +571,7 @@ Core does not create a `users` table. Authentication and user management are the
|
|
|
536
571
|
|------------|--------------|----------------------------------|
|
|
537
572
|
| id | UUID | Primary key |
|
|
538
573
|
| user_id | UUID | Optional owner/tenant identifier |
|
|
574
|
+
| organization_id | UUID | Optional owning organization; receives events for all its links |
|
|
539
575
|
| name | VARCHAR(255) | Webhook name |
|
|
540
576
|
| url | TEXT | Delivery URL |
|
|
541
577
|
| secret | VARCHAR(255) | HMAC signing secret |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FastifyInstance } from 'fastify';
|
|
2
2
|
import { DatabaseOptions } from './lib/database.js';
|
|
3
3
|
import { type RedirectRouteOptions } from './routes/redirect.js';
|
|
4
|
+
import { type SdkRouteOptions } from './routes/sdk.js';
|
|
4
5
|
/**
|
|
5
6
|
* Configuration options for creating a LinkForty server instance.
|
|
6
7
|
*/
|
|
@@ -17,6 +18,8 @@ export interface ServerOptions {
|
|
|
17
18
|
trustProxy?: boolean | number;
|
|
18
19
|
/** Options for the redirect routes: abuse-report link, launchpad page hooks. */
|
|
19
20
|
redirect?: RedirectRouteOptions;
|
|
21
|
+
/** Options for the SDK routes: install match scoping. */
|
|
22
|
+
sdk?: SdkRouteOptions;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Create and configure a LinkForty Fastify server instance.
|
|
@@ -33,10 +36,12 @@ export * from './lib/client-ip.js';
|
|
|
33
36
|
export * from './lib/database.js';
|
|
34
37
|
export * from './lib/fingerprint.js';
|
|
35
38
|
export * from './lib/webhook.js';
|
|
39
|
+
export * from './lib/link-lookup.js';
|
|
36
40
|
export * from './lib/link-safety.js';
|
|
37
41
|
export * from './lib/launchpad.js';
|
|
38
42
|
export * from './lib/event-emitter.js';
|
|
39
43
|
export * from './types/index.js';
|
|
40
44
|
export type { RedirectRouteOptions } from './routes/redirect.js';
|
|
45
|
+
export type { SdkRouteOptions } from './routes/sdk.js';
|
|
41
46
|
export { redirectRoutes, linkRoutes, analyticsRoutes, sdkRoutes, webhookRoutes, templateRoutes, qrRoutes, previewRoutes, debugRoutes, wellKnownRoutes, healthRoutes } from './routes/index.js';
|
|
42
47
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGnD,OAAO,EAAsB,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGjF,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAOlE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;KAC3B,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oJAAoJ;IACpJ,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,yDAAyD;IACzD,GAAG,CAAC,EAAE,eAAe,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,kTAiC7D;AAGD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function createServer(options = {}) {
|
|
|
43
43
|
await fastify.register(redirectRoutes, options.redirect ?? {});
|
|
44
44
|
await fastify.register(linkRoutes);
|
|
45
45
|
await fastify.register(analyticsRoutes);
|
|
46
|
-
await fastify.register(sdkRoutes);
|
|
46
|
+
await fastify.register(sdkRoutes, options.sdk ?? {});
|
|
47
47
|
await fastify.register(webhookRoutes);
|
|
48
48
|
await fastify.register(templateRoutes);
|
|
49
49
|
await fastify.register(qrRoutes);
|
|
@@ -55,6 +55,7 @@ export * from './lib/client-ip.js';
|
|
|
55
55
|
export * from './lib/database.js';
|
|
56
56
|
export * from './lib/fingerprint.js';
|
|
57
57
|
export * from './lib/webhook.js';
|
|
58
|
+
export * from './lib/link-lookup.js';
|
|
58
59
|
export * from './lib/link-safety.js';
|
|
59
60
|
export * from './lib/launchpad.js';
|
|
60
61
|
export * from './lib/event-emitter.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAmB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,cAAc,EAA6B,MAAM,sBAAsB,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/database.d.ts
CHANGED
|
@@ -14,5 +14,7 @@ export declare let db: pg.Pool;
|
|
|
14
14
|
* database can avoid colliding with this one. Stable across releases.
|
|
15
15
|
*/
|
|
16
16
|
export declare const SCHEMA_INIT_LOCK_KEY = 4977261;
|
|
17
|
+
/** Advisory lock key held while building idx_fingerprints_ip. Stable across releases. */
|
|
18
|
+
export declare const FINGERPRINT_IP_INDEX_LOCK_KEY = 4977262;
|
|
17
19
|
export declare function initializeDatabase(options?: DatabaseOptions): Promise<void>;
|
|
18
20
|
//# sourceMappingURL=database.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA4BvB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC;AA4BvB;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,UAAU,CAAC;AAGrD,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,eAAoB,iBA8qBrE"}
|
package/dist/lib/database.js
CHANGED
|
@@ -34,6 +34,8 @@ async function connectWithRetry(maxRetries = 10, baseDelay = 1000) {
|
|
|
34
34
|
* database can avoid colliding with this one. Stable across releases.
|
|
35
35
|
*/
|
|
36
36
|
export const SCHEMA_INIT_LOCK_KEY = 4977261;
|
|
37
|
+
/** Advisory lock key held while building idx_fingerprints_ip. Stable across releases. */
|
|
38
|
+
export const FINGERPRINT_IP_INDEX_LOCK_KEY = 4977262;
|
|
37
39
|
// Initialize database schema
|
|
38
40
|
export async function initializeDatabase(options = {}) {
|
|
39
41
|
// Initialize pool
|
|
@@ -109,7 +111,7 @@ export async function initializeDatabase(options = {}) {
|
|
|
109
111
|
CREATE TABLE IF NOT EXISTS links (
|
|
110
112
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
111
113
|
user_id UUID,
|
|
112
|
-
short_code VARCHAR(20)
|
|
114
|
+
short_code VARCHAR(20) NOT NULL,
|
|
113
115
|
original_url TEXT NOT NULL,
|
|
114
116
|
title VARCHAR(255),
|
|
115
117
|
description TEXT,
|
|
@@ -222,6 +224,11 @@ export async function initializeDatabase(options = {}) {
|
|
|
222
224
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
223
225
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
224
226
|
)
|
|
227
|
+
`);
|
|
228
|
+
// A webhook can belong to an organization, in which case it hears about every
|
|
229
|
+
// link the organization owns (findWebhooksForLink). Null scopes it to its user.
|
|
230
|
+
await client.query(`
|
|
231
|
+
ALTER TABLE webhooks ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE
|
|
225
232
|
`);
|
|
226
233
|
// Add organization_id column to links table.
|
|
227
234
|
//
|
|
@@ -240,6 +247,36 @@ export async function initializeDatabase(options = {}) {
|
|
|
240
247
|
END IF;
|
|
241
248
|
END $$;
|
|
242
249
|
`);
|
|
250
|
+
// Short codes are unique per organization; links without one share a
|
|
251
|
+
// single namespace (lib/link-lookup.ts describes how URLs resolve).
|
|
252
|
+
// The per-organization index is created before any single-column unique
|
|
253
|
+
// constraint on short_code is dropped, so there is never a moment without
|
|
254
|
+
// one. idx_links_short_code_lookup serves lookups by code alone.
|
|
255
|
+
await client.query(`
|
|
256
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_links_org_short_code
|
|
257
|
+
ON links ((COALESCE(organization_id, '00000000-0000-0000-0000-000000000000'::uuid)), short_code)
|
|
258
|
+
`);
|
|
259
|
+
await client.query('CREATE INDEX IF NOT EXISTS idx_links_short_code_lookup ON links(short_code)');
|
|
260
|
+
await client.query(`
|
|
261
|
+
DO $$
|
|
262
|
+
DECLARE
|
|
263
|
+
c record;
|
|
264
|
+
BEGIN
|
|
265
|
+
FOR c IN
|
|
266
|
+
SELECT con.conname
|
|
267
|
+
FROM pg_constraint con
|
|
268
|
+
WHERE con.conrelid = 'links'::regclass
|
|
269
|
+
AND con.contype = 'u'
|
|
270
|
+
AND con.conkey = ARRAY[(
|
|
271
|
+
SELECT attnum FROM pg_attribute
|
|
272
|
+
WHERE attrelid = 'links'::regclass AND attname = 'short_code'
|
|
273
|
+
)]::smallint[]
|
|
274
|
+
LOOP
|
|
275
|
+
EXECUTE format('ALTER TABLE links DROP CONSTRAINT %I', c.conname);
|
|
276
|
+
END LOOP;
|
|
277
|
+
END $$;
|
|
278
|
+
`);
|
|
279
|
+
await client.query('DROP INDEX IF EXISTS idx_links_short_code');
|
|
243
280
|
// Add template_id column to links table
|
|
244
281
|
await client.query(`
|
|
245
282
|
DO $$
|
|
@@ -584,7 +621,6 @@ export async function initializeDatabase(options = {}) {
|
|
|
584
621
|
END $$;
|
|
585
622
|
`);
|
|
586
623
|
// Create indexes for performance
|
|
587
|
-
await client.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_links_short_code ON links(short_code)');
|
|
588
624
|
await client.query('CREATE INDEX IF NOT EXISTS idx_links_user_id ON links(user_id)');
|
|
589
625
|
await client.query('CREATE INDEX IF NOT EXISTS idx_links_created_at ON links(created_at DESC)');
|
|
590
626
|
await client.query('CREATE INDEX IF NOT EXISTS idx_clicks_link_id ON click_events(link_id)');
|
|
@@ -607,6 +643,7 @@ export async function initializeDatabase(options = {}) {
|
|
|
607
643
|
// Indexes for webhooks
|
|
608
644
|
await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_user_id ON webhooks(user_id)');
|
|
609
645
|
await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active) WHERE is_active = true');
|
|
646
|
+
await client.query('CREATE INDEX IF NOT EXISTS idx_webhooks_organization_id ON webhooks(organization_id)');
|
|
610
647
|
// Indexes for in-app events
|
|
611
648
|
await client.query('CREATE INDEX IF NOT EXISTS idx_in_app_events_install_id ON in_app_events(install_id)');
|
|
612
649
|
await client.query('CREATE INDEX IF NOT EXISTS idx_in_app_events_name ON in_app_events(event_name)');
|
|
@@ -644,5 +681,58 @@ export async function initializeDatabase(options = {}) {
|
|
|
644
681
|
}
|
|
645
682
|
client.release();
|
|
646
683
|
}
|
|
684
|
+
await ensureFingerprintIpIndex();
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Index device_fingerprints by IP, which install matching selects candidate
|
|
688
|
+
* clicks on (matchInstallToClick).
|
|
689
|
+
*
|
|
690
|
+
* Built CONCURRENTLY because the table takes a write on every click and can be
|
|
691
|
+
* large, and a plain CREATE INDEX would hold those writes
|
|
692
|
+
* for the length of the build. That rules out the schema lock above: a
|
|
693
|
+
* concurrent build waits for every older snapshot to finish, including that of
|
|
694
|
+
* another instance blocked on the lock, which would wait on us in turn.
|
|
695
|
+
*
|
|
696
|
+
* A concurrent build that fails leaves an invalid index behind, which IF NOT
|
|
697
|
+
* EXISTS would then skip forever, so an invalid one is dropped and rebuilt. A
|
|
698
|
+
* build in progress also reads as invalid, hence the try-lock: only one instance
|
|
699
|
+
* ever works on the index, and the others move on rather than wait. Failure is
|
|
700
|
+
* logged, not thrown: matching is correct without the index, only slower.
|
|
701
|
+
*/
|
|
702
|
+
async function ensureFingerprintIpIndex() {
|
|
703
|
+
let client;
|
|
704
|
+
let lockHeld = false;
|
|
705
|
+
try {
|
|
706
|
+
client = await db.connect();
|
|
707
|
+
const lock = await client.query('SELECT pg_try_advisory_lock($1) AS acquired', [FINGERPRINT_IP_INDEX_LOCK_KEY]);
|
|
708
|
+
lockHeld = lock.rows[0]?.acquired === true;
|
|
709
|
+
if (!lockHeld) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const existing = await client.query(`SELECT i.indisvalid
|
|
713
|
+
FROM pg_class c
|
|
714
|
+
INNER JOIN pg_index i ON i.indexrelid = c.oid
|
|
715
|
+
WHERE c.relname = 'idx_fingerprints_ip'`);
|
|
716
|
+
if (existing.rows[0]?.indisvalid) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (existing.rows[0]) {
|
|
720
|
+
await client.query('DROP INDEX CONCURRENTLY IF EXISTS idx_fingerprints_ip');
|
|
721
|
+
}
|
|
722
|
+
await client.query('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fingerprints_ip ON device_fingerprints(ip_address)');
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
console.error('Could not build idx_fingerprints_ip; install matching will be slower:', error);
|
|
726
|
+
}
|
|
727
|
+
finally {
|
|
728
|
+
if (client) {
|
|
729
|
+
if (lockHeld) {
|
|
730
|
+
await client
|
|
731
|
+
.query('SELECT pg_advisory_unlock($1)', [FINGERPRINT_IP_INDEX_LOCK_KEY])
|
|
732
|
+
.catch(() => undefined);
|
|
733
|
+
}
|
|
734
|
+
client.release();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
647
737
|
}
|
|
648
738
|
//# sourceMappingURL=database.js.map
|
package/dist/lib/database.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/lib/database.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,6 +36,25 @@ export declare const DEFAULT_ATTRIBUTION_WINDOW_HOURS = 168;
|
|
|
36
36
|
* Minimum confidence threshold for attribution (70%)
|
|
37
37
|
*/
|
|
38
38
|
export declare const CONFIDENCE_THRESHOLD = 70;
|
|
39
|
+
/**
|
|
40
|
+
* The most a candidate can score without the IP signal. Below the threshold, so
|
|
41
|
+
* an IP match is required for any attribution — which is what lets
|
|
42
|
+
* matchInstallToClick fetch only the clicks from the install's own network.
|
|
43
|
+
* (Across SDK and browser contexts, platform + OS version stand in for the user
|
|
44
|
+
* agent and carry the same weight.)
|
|
45
|
+
*/
|
|
46
|
+
export declare const MAX_SCORE_WITHOUT_IP: number;
|
|
47
|
+
/**
|
|
48
|
+
* Narrows the clicks an install can be matched against.
|
|
49
|
+
*/
|
|
50
|
+
export interface InstallMatchScope {
|
|
51
|
+
/**
|
|
52
|
+
* Only clicks on links owned by this organization. A deployment serving
|
|
53
|
+
* several apps passes the installing app's organization, so an install can
|
|
54
|
+
* never be attributed to — or receive the deep link of — another app's link.
|
|
55
|
+
*/
|
|
56
|
+
organizationId?: string;
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Generate a fingerprint hash from device data
|
|
41
60
|
* Uses SHA-256 hash of concatenated device attributes
|
|
@@ -48,6 +67,13 @@ export declare function generateFingerprintHash(data: FingerprintData): string;
|
|
|
48
67
|
* must not contribute to a fingerprint match. Public IPs return true.
|
|
49
68
|
*/
|
|
50
69
|
export declare function isAttributableIp(ip: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The network an IP belongs to for matching purposes: its /24 for IPv4, its /64
|
|
72
|
+
* for IPv6 (the prefix a single household or device is normally assigned).
|
|
73
|
+
* Returned as CIDR text, so the same value both compares two addresses here and
|
|
74
|
+
* selects candidate clicks in SQL. Null when the address cannot be parsed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function attributionNetwork(ip: string): string | null;
|
|
51
77
|
/**
|
|
52
78
|
* Calculate confidence score by comparing two fingerprints
|
|
53
79
|
* Returns a score from 0-100 based on matched components
|
|
@@ -62,7 +88,7 @@ export declare function calculateConfidenceScore(fingerprint1: FingerprintData,
|
|
|
62
88
|
*
|
|
63
89
|
* Note: Uses link-specific attribution windows - each link can have its own window
|
|
64
90
|
*/
|
|
65
|
-
export declare function matchInstallToClick(installFingerprint: FingerprintData, attributionWindowHours?: number): Promise<FingerprintMatch | null>;
|
|
91
|
+
export declare function matchInstallToClick(installFingerprint: FingerprintData, attributionWindowHours?: number, scope?: InstallMatchScope): Promise<FingerprintMatch | null>;
|
|
66
92
|
/**
|
|
67
93
|
* Store device fingerprint for a click event
|
|
68
94
|
*/
|
|
@@ -73,7 +99,7 @@ export declare function storeFingerprintForClick(clickId: string, fingerprintDat
|
|
|
73
99
|
export declare function recordInstallEvent(fingerprintData: FingerprintData, deviceId?: string, attributionWindowHours?: number, sdk?: {
|
|
74
100
|
name?: string | null;
|
|
75
101
|
version?: string | null;
|
|
76
|
-
}): Promise<{
|
|
102
|
+
}, scope?: InstallMatchScope): Promise<{
|
|
77
103
|
installId: string;
|
|
78
104
|
match: FingerprintMatch | null;
|
|
79
105
|
deepLinkData: DeepLinkPayload | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/lib/fingerprint.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5C;AAwBD;;GAEG;AACH,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAcrE;AAwCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAyBpD;
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/lib/fingerprint.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5C;AAwBD;;GAEG;AACH,eAAO,MAAM,gCAAgC,MAAM,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,QAIM,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAcrE;AAwCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAyBpD;AAiCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AA6GD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,eAAe,EAC7B,YAAY,EAAE,eAAe,GAC5B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAkG7C;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,kBAAkB,EAAE,eAAe,EACnC,sBAAsB,GAAE,MAAyC,EACjE,KAAK,GAAE,iBAAsB,GAC5B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA0GlC;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,eAAe,GAC/B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,eAAe,EAChC,QAAQ,CAAC,EAAE,MAAM,EACjB,sBAAsB,GAAE,MAAyC,EACjE,GAAG,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACvD,KAAK,GAAE,iBAAsB,GAC5B,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC,CAAC,CAiJD"}
|
package/dist/lib/fingerprint.js
CHANGED
|
@@ -29,6 +29,17 @@ export const DEFAULT_ATTRIBUTION_WINDOW_HOURS = 168;
|
|
|
29
29
|
* Minimum confidence threshold for attribution (70%)
|
|
30
30
|
*/
|
|
31
31
|
export const CONFIDENCE_THRESHOLD = 70;
|
|
32
|
+
/**
|
|
33
|
+
* The most a candidate can score without the IP signal. Below the threshold, so
|
|
34
|
+
* an IP match is required for any attribution — which is what lets
|
|
35
|
+
* matchInstallToClick fetch only the clicks from the install's own network.
|
|
36
|
+
* (Across SDK and browser contexts, platform + OS version stand in for the user
|
|
37
|
+
* agent and carry the same weight.)
|
|
38
|
+
*/
|
|
39
|
+
export const MAX_SCORE_WITHOUT_IP = FINGERPRINT_WEIGHTS.USER_AGENT +
|
|
40
|
+
FINGERPRINT_WEIGHTS.TIMEZONE +
|
|
41
|
+
FINGERPRINT_WEIGHTS.LANGUAGE +
|
|
42
|
+
FINGERPRINT_WEIGHTS.SCREEN_RESOLUTION;
|
|
32
43
|
/**
|
|
33
44
|
* Generate a fingerprint hash from device data
|
|
34
45
|
* Uses SHA-256 hash of concatenated device attributes
|
|
@@ -119,20 +130,60 @@ export function isAttributableIp(ip) {
|
|
|
119
130
|
// Not a recognizable IPv4 or IPv6 address.
|
|
120
131
|
return false;
|
|
121
132
|
}
|
|
122
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Expand an IPv6 address to its eight hextets, lowercased and without leading
|
|
135
|
+
* zeros, so two spellings of the same address compare equal. An embedded IPv4
|
|
136
|
+
* tail (`64:ff9b::1.2.3.4`) becomes its two hextets. Returns null for anything
|
|
137
|
+
* that is not an IPv6 address.
|
|
138
|
+
*/
|
|
139
|
+
function expandIPv6(ip) {
|
|
140
|
+
let addr = ip.toLowerCase().split('%')[0]; // drop a zone index
|
|
141
|
+
const lastColon = addr.lastIndexOf(':');
|
|
142
|
+
const tail = addr.slice(lastColon + 1);
|
|
143
|
+
if (tail.includes('.')) {
|
|
144
|
+
const n = ipv4ToInt(tail);
|
|
145
|
+
if (n === null)
|
|
146
|
+
return null;
|
|
147
|
+
addr = `${addr.slice(0, lastColon + 1)}${(n >>> 16).toString(16)}:${(n & 0xffff).toString(16)}`;
|
|
148
|
+
}
|
|
149
|
+
const halves = addr.split('::');
|
|
150
|
+
if (halves.length > 2)
|
|
151
|
+
return null;
|
|
152
|
+
const head = halves[0] ? halves[0].split(':') : [];
|
|
153
|
+
const rest = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
|
|
154
|
+
const missing = 8 - head.length - rest.length;
|
|
155
|
+
if (halves.length === 2 ? missing < 1 : missing !== 0)
|
|
156
|
+
return null;
|
|
157
|
+
const groups = [...head, ...Array(missing).fill('0'), ...rest];
|
|
158
|
+
if (!groups.every((g) => /^[0-9a-f]{1,4}$/.test(g)))
|
|
159
|
+
return null;
|
|
160
|
+
return groups.map((g) => parseInt(g, 16).toString(16));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The network an IP belongs to for matching purposes: its /24 for IPv4, its /64
|
|
164
|
+
* for IPv6 (the prefix a single household or device is normally assigned).
|
|
165
|
+
* Returned as CIDR text, so the same value both compares two addresses here and
|
|
166
|
+
* selects candidate clicks in SQL. Null when the address cannot be parsed.
|
|
167
|
+
*/
|
|
168
|
+
export function attributionNetwork(ip) {
|
|
123
169
|
if (!ip)
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
170
|
+
return null;
|
|
171
|
+
let addr = ip.trim();
|
|
172
|
+
if (addr.toLowerCase().startsWith('::ffff:') && addr.includes('.'))
|
|
173
|
+
addr = addr.slice(7);
|
|
174
|
+
if (addr.includes('.') && !addr.includes(':')) {
|
|
175
|
+
const n = ipv4ToInt(addr);
|
|
176
|
+
if (n === null)
|
|
177
|
+
return null;
|
|
178
|
+
return `${n >>> 24}.${(n >>> 16) & 0xff}.${(n >>> 8) & 0xff}.0/24`;
|
|
129
179
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
if (addr.includes(':')) {
|
|
181
|
+
const groups = expandIPv6(addr);
|
|
182
|
+
if (!groups)
|
|
183
|
+
return null;
|
|
184
|
+
return `${groups.slice(0, 4).join(':')}::/64`;
|
|
134
185
|
}
|
|
135
|
-
return
|
|
186
|
+
return null;
|
|
136
187
|
}
|
|
137
188
|
/**
|
|
138
189
|
* Normalize user agent for comparison
|
|
@@ -236,9 +287,9 @@ export function calculateConfidenceScore(fingerprint1, fingerprint2) {
|
|
|
236
287
|
fingerprint2.ipAddress &&
|
|
237
288
|
isAttributableIp(fingerprint1.ipAddress) &&
|
|
238
289
|
isAttributableIp(fingerprint2.ipAddress)) {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
290
|
+
const network1 = attributionNetwork(fingerprint1.ipAddress);
|
|
291
|
+
const network2 = attributionNetwork(fingerprint2.ipAddress);
|
|
292
|
+
if (network1 !== null && network1 === network2) {
|
|
242
293
|
score += FINGERPRINT_WEIGHTS.IP_ADDRESS;
|
|
243
294
|
matchedFactors.push('ip');
|
|
244
295
|
}
|
|
@@ -314,11 +365,31 @@ export function calculateConfidenceScore(fingerprint1, fingerprint2) {
|
|
|
314
365
|
*
|
|
315
366
|
* Note: Uses link-specific attribution windows - each link can have its own window
|
|
316
367
|
*/
|
|
317
|
-
export async function matchInstallToClick(installFingerprint, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS) {
|
|
368
|
+
export async function matchInstallToClick(installFingerprint, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS, scope = {}) {
|
|
369
|
+
// No candidate reaches the threshold without matching the install's network
|
|
370
|
+
// (MAX_SCORE_WITHOUT_IP), so an install from a shared or unparseable address
|
|
371
|
+
// cannot be attributed, and the only clicks worth scoring are the ones from
|
|
372
|
+
// its own /24 or /64. Selecting those in SQL keeps every click within the
|
|
373
|
+
// attribution window in reach, however much traffic the server takes.
|
|
374
|
+
if (!isAttributableIp(installFingerprint.ipAddress)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const network = attributionNetwork(installFingerprint.ipAddress);
|
|
378
|
+
if (!network) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
318
381
|
// Query recent click events within maximum possible attribution window (90 days)
|
|
319
382
|
// We'll validate against each link's specific window during matching
|
|
320
383
|
const maxWindowHours = 2160; // 90 days
|
|
321
384
|
const cutoffTime = new Date(Date.now() - maxWindowHours * 60 * 60 * 1000);
|
|
385
|
+
const params = [cutoffTime, network];
|
|
386
|
+
let scopeFilter = '';
|
|
387
|
+
if (scope.organizationId) {
|
|
388
|
+
params.push(scope.organizationId);
|
|
389
|
+
scopeFilter = `AND l.organization_id = $${params.length}`;
|
|
390
|
+
}
|
|
391
|
+
// The LIMIT only bounds work for a very busy network (a campus, an office);
|
|
392
|
+
// it applies after the network filter, so ordinary traffic never reaches it.
|
|
322
393
|
const clicksResult = await db.query(`SELECT
|
|
323
394
|
ce.id as click_id,
|
|
324
395
|
ce.link_id,
|
|
@@ -333,12 +404,14 @@ export async function matchInstallToClick(installFingerprint, attributionWindowH
|
|
|
333
404
|
df.screen_height,
|
|
334
405
|
df.platform,
|
|
335
406
|
df.platform_version
|
|
336
|
-
FROM
|
|
337
|
-
INNER JOIN
|
|
407
|
+
FROM device_fingerprints df
|
|
408
|
+
INNER JOIN click_events ce ON ce.id = df.click_id
|
|
338
409
|
INNER JOIN links l ON ce.link_id = l.id
|
|
339
|
-
WHERE
|
|
410
|
+
WHERE df.ip_address <<= $2::cidr
|
|
411
|
+
AND ce.clicked_at >= $1
|
|
412
|
+
${scopeFilter}
|
|
340
413
|
ORDER BY ce.clicked_at DESC
|
|
341
|
-
LIMIT 1000`,
|
|
414
|
+
LIMIT 1000`, params);
|
|
342
415
|
if (clicksResult.rows.length === 0) {
|
|
343
416
|
return null;
|
|
344
417
|
}
|
|
@@ -413,10 +486,10 @@ export async function storeFingerprintForClick(clickId, fingerprintData) {
|
|
|
413
486
|
/**
|
|
414
487
|
* Record an install event and attempt to match it to a click
|
|
415
488
|
*/
|
|
416
|
-
export async function recordInstallEvent(fingerprintData, deviceId, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS, sdk) {
|
|
489
|
+
export async function recordInstallEvent(fingerprintData, deviceId, attributionWindowHours = DEFAULT_ATTRIBUTION_WINDOW_HOURS, sdk, scope = {}) {
|
|
417
490
|
const fingerprintHash = generateFingerprintHash(fingerprintData);
|
|
418
491
|
// Attempt to match install to a click
|
|
419
|
-
const match = await matchInstallToClick(fingerprintData, attributionWindowHours);
|
|
492
|
+
const match = await matchInstallToClick(fingerprintData, attributionWindowHours, scope);
|
|
420
493
|
// Attribution metadata for measurement: install attribution is
|
|
421
494
|
// fingerprint-based, so the method is 'fingerprint' on a match and 'none'
|
|
422
495
|
// (organic) otherwise; matched_factors records which signals matched.
|
|
@@ -506,28 +579,21 @@ export async function recordInstallEvent(fingerprintData, deviceId, attributionW
|
|
|
506
579
|
WHERE id = $2`, [JSON.stringify(deepLinkData), installId]);
|
|
507
580
|
// Trigger webhooks for install_event (only if attributed)
|
|
508
581
|
try {
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
userAgent: fingerprintData.userAgent,
|
|
525
|
-
platform: fingerprintData.platform,
|
|
526
|
-
};
|
|
527
|
-
// Trigger webhooks without delivery logging (basic version)
|
|
528
|
-
// For delivery logging, use @linkforty/cloud premium features
|
|
529
|
-
await triggerWebhooks(webhooksResult.rows, 'install_event', installId, installEventData);
|
|
530
|
-
}
|
|
582
|
+
const { findWebhooksForLink, triggerWebhooks } = await import('./webhook.js');
|
|
583
|
+
const webhooks = await findWebhooksForLink(match.linkId);
|
|
584
|
+
if (webhooks.length > 0) {
|
|
585
|
+
const installEventData = {
|
|
586
|
+
id: installId,
|
|
587
|
+
linkId: match.linkId,
|
|
588
|
+
fingerprintHash,
|
|
589
|
+
confidenceScore: match.confidenceScore,
|
|
590
|
+
installedAt: new Date().toISOString(),
|
|
591
|
+
deepLinkData,
|
|
592
|
+
ipAddress: fingerprintData.ipAddress,
|
|
593
|
+
userAgent: fingerprintData.userAgent,
|
|
594
|
+
platform: fingerprintData.platform,
|
|
595
|
+
};
|
|
596
|
+
await triggerWebhooks(webhooks, 'install_event', installId, installEventData);
|
|
531
597
|
}
|
|
532
598
|
}
|
|
533
599
|
catch (webhookError) {
|