디퍼드 딥링크에서 매칭이 0으로 나오는 문제 수정
This commit is contained in:
@@ -147,6 +147,38 @@ describe('calculateConfidenceScore', () => {
|
|||||||
expect(score).toBe(20);
|
expect(score).toBe(20);
|
||||||
expect(matchedFactors.sort()).toEqual(['screen', 'timezone'].sort());
|
expect(matchedFactors.sort()).toEqual(['screen', 'timezone'].sort());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches a mobile browser click to a native-app install on platform + device signals', () => {
|
||||||
|
const click = {
|
||||||
|
ipAddress: '100.64.0.5',
|
||||||
|
userAgent:
|
||||||
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1',
|
||||||
|
timezone: 'Asia/Seoul',
|
||||||
|
language: 'ko-KR',
|
||||||
|
screenWidth: 1179,
|
||||||
|
screenHeight: 2556,
|
||||||
|
platform: 'ios',
|
||||||
|
platformVersion: '17.4',
|
||||||
|
};
|
||||||
|
|
||||||
|
const install = {
|
||||||
|
ipAddress: '100.64.0.9',
|
||||||
|
userAgent: 'LinkFortySDK/1.4.0 (iOS 17.4; iPhone15,3)',
|
||||||
|
timezone: 'Asia/Seoul',
|
||||||
|
language: 'ko-KR',
|
||||||
|
screenWidth: 1179,
|
||||||
|
screenHeight: 2556,
|
||||||
|
platform: 'iOS',
|
||||||
|
platformVersion: '17.4',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { score, matchedFactors } = fingerprint.calculateConfidenceScore(click, install);
|
||||||
|
|
||||||
|
expect(score).toBe(80);
|
||||||
|
expect(matchedFactors).toEqual(
|
||||||
|
expect.arrayContaining(['user_agent', 'platform', 'timezone', 'language', 'screen'])
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isAttributableIp', () => {
|
describe('isAttributableIp', () => {
|
||||||
|
|||||||
+241
-69
@@ -26,6 +26,42 @@ export interface FingerprintMatch {
|
|||||||
clickedAt: Date;
|
clickedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FingerprintFactorDebug = {
|
||||||
|
ip: {
|
||||||
|
matched: boolean;
|
||||||
|
installAttributable: boolean;
|
||||||
|
clickAttributable: boolean;
|
||||||
|
installValue: string;
|
||||||
|
clickValue: string;
|
||||||
|
};
|
||||||
|
userAgent: {
|
||||||
|
matched: boolean;
|
||||||
|
installValue: string;
|
||||||
|
clickValue: string;
|
||||||
|
mobilePlatformBridge: boolean;
|
||||||
|
};
|
||||||
|
platform: {
|
||||||
|
matched: boolean;
|
||||||
|
installValue: string;
|
||||||
|
clickValue: string;
|
||||||
|
};
|
||||||
|
timezone: {
|
||||||
|
matched: boolean;
|
||||||
|
installValue: string;
|
||||||
|
clickValue: string;
|
||||||
|
};
|
||||||
|
language: {
|
||||||
|
matched: boolean;
|
||||||
|
installValue: string;
|
||||||
|
clickValue: string;
|
||||||
|
};
|
||||||
|
screen: {
|
||||||
|
matched: boolean;
|
||||||
|
installValue: string;
|
||||||
|
clickValue: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scoring weights for probabilistic matching
|
* Scoring weights for probabilistic matching
|
||||||
* Total should equal 100 for percentage-based confidence
|
* Total should equal 100 for percentage-based confidence
|
||||||
@@ -36,6 +72,7 @@ const FINGERPRINT_WEIGHTS = {
|
|||||||
TIMEZONE: 10,
|
TIMEZONE: 10,
|
||||||
LANGUAGE: 10,
|
LANGUAGE: 10,
|
||||||
SCREEN_RESOLUTION: 10,
|
SCREEN_RESOLUTION: 10,
|
||||||
|
PLATFORM: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +84,7 @@ export const DEFAULT_ATTRIBUTION_WINDOW_HOURS = 168;
|
|||||||
* Minimum confidence threshold for attribution (70%)
|
* Minimum confidence threshold for attribution (70%)
|
||||||
*/
|
*/
|
||||||
export const CONFIDENCE_THRESHOLD = 70;
|
export const CONFIDENCE_THRESHOLD = 70;
|
||||||
|
const ATTRIBUTION_DEBUG_ENV = 'LINKFORTY_DEBUG_ATTRIBUTION';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a fingerprint hash from device data
|
* Generate a fingerprint hash from device data
|
||||||
@@ -175,6 +213,157 @@ function normalizeUserAgent(ua: string): string {
|
|||||||
return `${platform}|${browser}`.toLowerCase();
|
return `${platform}|${browser}`.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractPlatformFromUserAgent(ua: string): string {
|
||||||
|
if (!ua) return '';
|
||||||
|
|
||||||
|
const platformMatch = ua.match(/(iPhone|iPad|iOS|Android|Windows|Macintosh|Linux)/i);
|
||||||
|
return platformMatch ? normalizePlatform(platformMatch[1]) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlatform(platform: string | undefined): string {
|
||||||
|
if (!platform) return '';
|
||||||
|
|
||||||
|
const normalized = platform.trim().toLowerCase();
|
||||||
|
if (!normalized) return '';
|
||||||
|
if (['iphone', 'ipad', 'ios'].includes(normalized)) return 'ios';
|
||||||
|
if (normalized === 'android') return 'android';
|
||||||
|
if (['mac', 'macos', 'macintosh'].includes(normalized)) return 'mac';
|
||||||
|
if (['win', 'windows'].includes(normalized)) return 'windows';
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePlatformFingerprint(data: FingerprintData): string {
|
||||||
|
return normalizePlatform(data.platform) || extractPlatformFromUserAgent(data.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMobilePlatform(platform: string): boolean {
|
||||||
|
return platform === 'ios' || platform === 'android';
|
||||||
|
}
|
||||||
|
|
||||||
|
function attributionDebugEnabled(): boolean {
|
||||||
|
const value = process.env[ATTRIBUTION_DEBUG_ENV]?.trim().toLowerCase();
|
||||||
|
return value === '1' || value === 'true' || value === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAttributionDebug(event: string, payload: Record<string, unknown>): void {
|
||||||
|
if (!attributionDebugEnabled()) return;
|
||||||
|
console.info(`[linkforty][attribution] ${event} ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreFingerprintMatch(
|
||||||
|
fingerprint1: FingerprintData,
|
||||||
|
fingerprint2: FingerprintData
|
||||||
|
): { score: number; matchedFactors: string[]; debug: FingerprintFactorDebug } {
|
||||||
|
let score = 0;
|
||||||
|
const matchedFactors: string[] = [];
|
||||||
|
const platform1 = resolvePlatformFingerprint(fingerprint1);
|
||||||
|
const platform2 = resolvePlatformFingerprint(fingerprint2);
|
||||||
|
const installIpAttributable = isAttributableIp(fingerprint1.ipAddress);
|
||||||
|
const clickIpAttributable = isAttributableIp(fingerprint2.ipAddress);
|
||||||
|
const ip1 = fingerprint1.ipAddress ? normalizeIP(fingerprint1.ipAddress) : '';
|
||||||
|
const ip2 = fingerprint2.ipAddress ? normalizeIP(fingerprint2.ipAddress) : '';
|
||||||
|
const ua1 = fingerprint1.userAgent ? normalizeUserAgent(fingerprint1.userAgent) : '';
|
||||||
|
const ua2 = fingerprint2.userAgent ? normalizeUserAgent(fingerprint2.userAgent) : '';
|
||||||
|
const mobilePlatformBridge = platform1 !== '' && platform1 === platform2 && isMobilePlatform(platform1);
|
||||||
|
const language1 = fingerprint1.language?.substring(0, 2).toLowerCase() || '';
|
||||||
|
const language2 = fingerprint2.language?.substring(0, 2).toLowerCase() || '';
|
||||||
|
const screen1 =
|
||||||
|
fingerprint1.screenWidth && fingerprint1.screenHeight
|
||||||
|
? `${fingerprint1.screenWidth}x${fingerprint1.screenHeight}`
|
||||||
|
: '';
|
||||||
|
const screen2 =
|
||||||
|
fingerprint2.screenWidth && fingerprint2.screenHeight
|
||||||
|
? `${fingerprint2.screenWidth}x${fingerprint2.screenHeight}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const debug: FingerprintFactorDebug = {
|
||||||
|
ip: {
|
||||||
|
matched: false,
|
||||||
|
installAttributable: installIpAttributable,
|
||||||
|
clickAttributable: clickIpAttributable,
|
||||||
|
installValue: ip1,
|
||||||
|
clickValue: ip2,
|
||||||
|
},
|
||||||
|
userAgent: {
|
||||||
|
matched: false,
|
||||||
|
installValue: ua1,
|
||||||
|
clickValue: ua2,
|
||||||
|
mobilePlatformBridge,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
matched: false,
|
||||||
|
installValue: platform1,
|
||||||
|
clickValue: platform2,
|
||||||
|
},
|
||||||
|
timezone: {
|
||||||
|
matched: false,
|
||||||
|
installValue: fingerprint1.timezone || '',
|
||||||
|
clickValue: fingerprint2.timezone || '',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
matched: false,
|
||||||
|
installValue: language1,
|
||||||
|
clickValue: language2,
|
||||||
|
},
|
||||||
|
screen: {
|
||||||
|
matched: false,
|
||||||
|
installValue: screen1,
|
||||||
|
clickValue: screen2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
fingerprint1.ipAddress &&
|
||||||
|
fingerprint2.ipAddress &&
|
||||||
|
installIpAttributable &&
|
||||||
|
clickIpAttributable &&
|
||||||
|
ip1 === ip2
|
||||||
|
) {
|
||||||
|
score += FINGERPRINT_WEIGHTS.IP_ADDRESS;
|
||||||
|
matchedFactors.push('ip');
|
||||||
|
debug.ip.matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fingerprint1.userAgent && fingerprint2.userAgent && (ua1 === ua2 || mobilePlatformBridge)) {
|
||||||
|
score += FINGERPRINT_WEIGHTS.USER_AGENT;
|
||||||
|
matchedFactors.push('user_agent');
|
||||||
|
debug.userAgent.matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobilePlatformBridge) {
|
||||||
|
score += FINGERPRINT_WEIGHTS.PLATFORM;
|
||||||
|
matchedFactors.push('platform');
|
||||||
|
debug.platform.matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fingerprint1.timezone && fingerprint2.timezone && fingerprint1.timezone === fingerprint2.timezone) {
|
||||||
|
score += FINGERPRINT_WEIGHTS.TIMEZONE;
|
||||||
|
matchedFactors.push('timezone');
|
||||||
|
debug.timezone.matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (language1 !== '' && language1 === language2) {
|
||||||
|
score += FINGERPRINT_WEIGHTS.LANGUAGE;
|
||||||
|
matchedFactors.push('language');
|
||||||
|
debug.language.matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fingerprint1.screenWidth &&
|
||||||
|
fingerprint1.screenHeight &&
|
||||||
|
fingerprint2.screenWidth &&
|
||||||
|
fingerprint2.screenHeight &&
|
||||||
|
fingerprint1.screenWidth === fingerprint2.screenWidth &&
|
||||||
|
fingerprint1.screenHeight === fingerprint2.screenHeight
|
||||||
|
) {
|
||||||
|
score += FINGERPRINT_WEIGHTS.SCREEN_RESOLUTION;
|
||||||
|
matchedFactors.push('screen');
|
||||||
|
debug.screen.matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { score: Math.min(score, 100), matchedFactors, debug };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate confidence score by comparing two fingerprints
|
* Calculate confidence score by comparing two fingerprints
|
||||||
* Returns a score from 0-100 based on matched components
|
* Returns a score from 0-100 based on matched components
|
||||||
@@ -183,74 +372,7 @@ export function calculateConfidenceScore(
|
|||||||
fingerprint1: FingerprintData,
|
fingerprint1: FingerprintData,
|
||||||
fingerprint2: FingerprintData
|
fingerprint2: FingerprintData
|
||||||
): { score: number; matchedFactors: string[] } {
|
): { score: number; matchedFactors: string[] } {
|
||||||
let score = 0;
|
const { score, matchedFactors } = scoreFingerprintMatch(fingerprint1, fingerprint2);
|
||||||
const matchedFactors: string[] = [];
|
|
||||||
|
|
||||||
// Compare IP addresses (normalized to /24 subnet for IPv4). Only count IPs
|
|
||||||
// that actually identify a device — shared/NAT ranges (CGNAT, RFC1918, etc.)
|
|
||||||
// are skipped so unrelated users behind the same NAT can't match on IP.
|
|
||||||
if (
|
|
||||||
fingerprint1.ipAddress &&
|
|
||||||
fingerprint2.ipAddress &&
|
|
||||||
isAttributableIp(fingerprint1.ipAddress) &&
|
|
||||||
isAttributableIp(fingerprint2.ipAddress)
|
|
||||||
) {
|
|
||||||
const ip1 = normalizeIP(fingerprint1.ipAddress);
|
|
||||||
const ip2 = normalizeIP(fingerprint2.ipAddress);
|
|
||||||
|
|
||||||
if (ip1 === ip2) {
|
|
||||||
score += FINGERPRINT_WEIGHTS.IP_ADDRESS;
|
|
||||||
matchedFactors.push('ip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare user agents (normalized to platform + browser)
|
|
||||||
if (fingerprint1.userAgent && fingerprint2.userAgent) {
|
|
||||||
const ua1 = normalizeUserAgent(fingerprint1.userAgent);
|
|
||||||
const ua2 = normalizeUserAgent(fingerprint2.userAgent);
|
|
||||||
|
|
||||||
if (ua1 === ua2) {
|
|
||||||
score += FINGERPRINT_WEIGHTS.USER_AGENT;
|
|
||||||
matchedFactors.push('user_agent');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare timezone
|
|
||||||
if (fingerprint1.timezone && fingerprint2.timezone) {
|
|
||||||
if (fingerprint1.timezone === fingerprint2.timezone) {
|
|
||||||
score += FINGERPRINT_WEIGHTS.TIMEZONE;
|
|
||||||
matchedFactors.push('timezone');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare language
|
|
||||||
if (fingerprint1.language && fingerprint2.language) {
|
|
||||||
// Match first 2 characters (e.g., "en-US" matches "en-GB")
|
|
||||||
const lang1 = fingerprint1.language.substring(0, 2).toLowerCase();
|
|
||||||
const lang2 = fingerprint2.language.substring(0, 2).toLowerCase();
|
|
||||||
|
|
||||||
if (lang1 === lang2) {
|
|
||||||
score += FINGERPRINT_WEIGHTS.LANGUAGE;
|
|
||||||
matchedFactors.push('language');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare screen resolution
|
|
||||||
if (
|
|
||||||
fingerprint1.screenWidth &&
|
|
||||||
fingerprint1.screenHeight &&
|
|
||||||
fingerprint2.screenWidth &&
|
|
||||||
fingerprint2.screenHeight
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
fingerprint1.screenWidth === fingerprint2.screenWidth &&
|
|
||||||
fingerprint1.screenHeight === fingerprint2.screenHeight
|
|
||||||
) {
|
|
||||||
score += FINGERPRINT_WEIGHTS.SCREEN_RESOLUTION;
|
|
||||||
matchedFactors.push('screen');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { score, matchedFactors };
|
return { score, matchedFactors };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,11 +446,38 @@ export async function matchInstallToClick(
|
|||||||
platformVersion: row.platform_version,
|
platformVersion: row.platform_version,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { score, matchedFactors } = calculateConfidenceScore(
|
const { score, matchedFactors, debug } = scoreFingerprintMatch(
|
||||||
installFingerprint,
|
installFingerprint,
|
||||||
clickFingerprint
|
clickFingerprint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logAttributionDebug('candidate_scored', {
|
||||||
|
clickId: row.click_id,
|
||||||
|
linkId: row.link_id,
|
||||||
|
score,
|
||||||
|
threshold: CONFIDENCE_THRESHOLD,
|
||||||
|
matchedFactors,
|
||||||
|
timeDiffHours,
|
||||||
|
attributionWindowHours: linkWindowHours,
|
||||||
|
installFingerprint: {
|
||||||
|
ipAddress: installFingerprint.ipAddress,
|
||||||
|
platform: installFingerprint.platform || null,
|
||||||
|
timezone: installFingerprint.timezone || null,
|
||||||
|
language: installFingerprint.language || null,
|
||||||
|
screenWidth: installFingerprint.screenWidth || null,
|
||||||
|
screenHeight: installFingerprint.screenHeight || null,
|
||||||
|
},
|
||||||
|
clickFingerprint: {
|
||||||
|
ipAddress: clickFingerprint.ipAddress,
|
||||||
|
platform: clickFingerprint.platform || null,
|
||||||
|
timezone: clickFingerprint.timezone || null,
|
||||||
|
language: clickFingerprint.language || null,
|
||||||
|
screenWidth: clickFingerprint.screenWidth || null,
|
||||||
|
screenHeight: clickFingerprint.screenHeight || null,
|
||||||
|
},
|
||||||
|
factors: debug,
|
||||||
|
});
|
||||||
|
|
||||||
// Track the best match
|
// Track the best match
|
||||||
if (score > highestScore && score >= CONFIDENCE_THRESHOLD) {
|
if (score > highestScore && score >= CONFIDENCE_THRESHOLD) {
|
||||||
highestScore = score;
|
highestScore = score;
|
||||||
@@ -342,6 +491,29 @@ export async function matchInstallToClick(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logAttributionDebug('install_match_result', {
|
||||||
|
matched: bestMatch !== null,
|
||||||
|
highestScore,
|
||||||
|
threshold: CONFIDENCE_THRESHOLD,
|
||||||
|
installFingerprint: {
|
||||||
|
ipAddress: installFingerprint.ipAddress,
|
||||||
|
platform: installFingerprint.platform || null,
|
||||||
|
timezone: installFingerprint.timezone || null,
|
||||||
|
language: installFingerprint.language || null,
|
||||||
|
screenWidth: installFingerprint.screenWidth || null,
|
||||||
|
screenHeight: installFingerprint.screenHeight || null,
|
||||||
|
},
|
||||||
|
bestMatch: bestMatch
|
||||||
|
? {
|
||||||
|
clickId: bestMatch.clickId,
|
||||||
|
linkId: bestMatch.linkId,
|
||||||
|
confidenceScore: bestMatch.confidenceScore,
|
||||||
|
matchedFactors: bestMatch.matchedFactors,
|
||||||
|
clickedAt: bestMatch.clickedAt.toISOString(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
|
||||||
return bestMatch;
|
return bestMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,8 @@ export async function redirectRoutes(fastify: FastifyInstance) {
|
|||||||
const fpLanguage = query?.fp_lang || acceptLanguage.split(',')[0]?.split(';')[0] || undefined;
|
const fpLanguage = query?.fp_lang || acceptLanguage.split(',')[0]?.split(';')[0] || undefined;
|
||||||
const fpScreenWidth = query?.fp_sw ? parseInt(query.fp_sw, 10) : undefined;
|
const fpScreenWidth = query?.fp_sw ? parseInt(query.fp_sw, 10) : undefined;
|
||||||
const fpScreenHeight = query?.fp_sh ? parseInt(query.fp_sh, 10) : undefined;
|
const fpScreenHeight = query?.fp_sh ? parseInt(query.fp_sh, 10) : undefined;
|
||||||
|
const fpPlatform = query?.fp_platform || deviceType;
|
||||||
|
const fpPlatformVersion = query?.fp_pv || platformVersion;
|
||||||
|
|
||||||
// Insert click event with the pre-generated id (see above) so the row
|
// Insert click event with the pre-generated id (see above) so the row
|
||||||
// matches the lf_click value already placed on the redirect URL.
|
// matches the lf_click value already placed on the redirect URL.
|
||||||
@@ -327,8 +329,8 @@ export async function redirectRoutes(fastify: FastifyInstance) {
|
|||||||
language: fpLanguage,
|
language: fpLanguage,
|
||||||
screenWidth: fpScreenWidth,
|
screenWidth: fpScreenWidth,
|
||||||
screenHeight: fpScreenHeight,
|
screenHeight: fpScreenHeight,
|
||||||
platform: deviceType,
|
platform: fpPlatform,
|
||||||
platformVersion,
|
platformVersion: fpPlatformVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
await storeFingerprintForClick(clickId, fingerprintData);
|
await storeFingerprintForClick(clickId, fingerprintData);
|
||||||
|
|||||||
+4
-2
@@ -609,6 +609,8 @@ export async function sdkRoutes(fastify: FastifyInstance) {
|
|||||||
const fpLanguage = query?.fp_lang || acceptLanguage.split(',')[0]?.split(';')[0] || undefined;
|
const fpLanguage = query?.fp_lang || acceptLanguage.split(',')[0]?.split(';')[0] || undefined;
|
||||||
const fpScreenWidth = query?.fp_sw ? parseInt(query.fp_sw, 10) : undefined;
|
const fpScreenWidth = query?.fp_sw ? parseInt(query.fp_sw, 10) : undefined;
|
||||||
const fpScreenHeight = query?.fp_sh ? parseInt(query.fp_sh, 10) : undefined;
|
const fpScreenHeight = query?.fp_sh ? parseInt(query.fp_sh, 10) : undefined;
|
||||||
|
const fpPlatform = query?.fp_platform || deviceType;
|
||||||
|
const fpPlatformVersion = query?.fp_pv || platformVersion;
|
||||||
|
|
||||||
// Insert click event
|
// Insert click event
|
||||||
const clickResult = await db.query(
|
const clickResult = await db.query(
|
||||||
@@ -650,8 +652,8 @@ export async function sdkRoutes(fastify: FastifyInstance) {
|
|||||||
language: fpLanguage,
|
language: fpLanguage,
|
||||||
screenWidth: fpScreenWidth,
|
screenWidth: fpScreenWidth,
|
||||||
screenHeight: fpScreenHeight,
|
screenHeight: fpScreenHeight,
|
||||||
platform: deviceType,
|
platform: fpPlatform,
|
||||||
platformVersion,
|
platformVersion: fpPlatformVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
await storeFingerprintForClick(clickId, fingerprintData);
|
await storeFingerprintForClick(clickId, fingerprintData);
|
||||||
|
|||||||
Reference in New Issue
Block a user