feat(geo): make geo a package instead of service (#161)
This commit is contained in:
committed by
GitHub
parent
f59bcfba3c
commit
34414e1d3e
@@ -1,4 +1,4 @@
|
||||
import { getClientIp, parseIp } from '@/utils/parse-ip';
|
||||
import { getClientIp } from '@/utils/get-client-ip';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
import { generateDeviceId } from '@openpanel/common/server';
|
||||
@@ -8,6 +8,7 @@ import { getLock } from '@openpanel/redis';
|
||||
import type { PostEventPayload } from '@openpanel/sdk';
|
||||
|
||||
import { checkDuplicatedEvent } from '@/utils/deduplicate';
|
||||
import { getGeoLocation } from '@openpanel/geo';
|
||||
import { getStringHeaders, getTimestamp } from './track.controller';
|
||||
|
||||
export async function postEvent(
|
||||
@@ -26,7 +27,7 @@ export async function postEvent(
|
||||
return;
|
||||
}
|
||||
|
||||
const [salts, geo] = await Promise.all([getSalts(), parseIp(ip)]);
|
||||
const [salts, geo] = await Promise.all([getSalts(), getGeoLocation(ip)]);
|
||||
const currentDeviceId = generateDeviceId({
|
||||
salt: salts.current,
|
||||
origin: projectId,
|
||||
|
||||
@@ -4,8 +4,10 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import icoToPng from 'ico-to-png';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { getClientIp } from '@/utils/get-client-ip';
|
||||
import { createHash } from '@openpanel/common/server';
|
||||
import { TABLE_NAMES, ch, chQuery, formatClickhouseDate } from '@openpanel/db';
|
||||
import { getGeoLocation } from '@openpanel/geo';
|
||||
import { cacheable, getCache, getRedisCache } from '@openpanel/redis';
|
||||
|
||||
interface GetFaviconParams {
|
||||
@@ -170,3 +172,12 @@ export async function stats(request: FastifyRequest, reply: FastifyReply) {
|
||||
eventsLast24hCount: res.last24hCount,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getGeo(request: FastifyRequest, reply: FastifyReply) {
|
||||
const ip = getClientIp(request);
|
||||
if (!ip) {
|
||||
return reply.status(400).send('Bad Request');
|
||||
}
|
||||
const geo = await getGeoLocation(ip);
|
||||
return reply.status(200).send(geo);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { getClientIp, parseIp } from '@/utils/parse-ip';
|
||||
import { getClientIp } from '@/utils/get-client-ip';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { assocPath, pathOr } from 'ramda';
|
||||
|
||||
import { checkDuplicatedEvent, isDuplicatedEvent } from '@/utils/deduplicate';
|
||||
import { parseUserAgent } from '@openpanel/common/server';
|
||||
import { getProfileById, upsertProfile } from '@openpanel/db';
|
||||
import { getGeoLocation } from '@openpanel/geo';
|
||||
import type {
|
||||
IncrementProfilePayload,
|
||||
UpdateProfilePayload,
|
||||
@@ -24,7 +25,7 @@ export async function updateProfile(
|
||||
const ip = getClientIp(request)!;
|
||||
const ua = request.headers['user-agent']!;
|
||||
const uaInfo = parseUserAgent(ua, properties);
|
||||
const geo = await parseIp(ip);
|
||||
const geo = await getGeoLocation(ip);
|
||||
|
||||
if (
|
||||
await checkDuplicatedEvent({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { GeoLocation } from '@/utils/parse-ip';
|
||||
import { getClientIp, parseIp } from '@/utils/parse-ip';
|
||||
import { getClientIp } from '@/utils/get-client-ip';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { path, assocPath, pathOr, pick } from 'ramda';
|
||||
|
||||
import { checkDuplicatedEvent, isDuplicatedEvent } from '@/utils/deduplicate';
|
||||
import { checkDuplicatedEvent } from '@/utils/deduplicate';
|
||||
import { generateDeviceId, parseUserAgent } from '@openpanel/common/server';
|
||||
import { getProfileById, getSalts, upsertProfile } from '@openpanel/db';
|
||||
import { type GeoLocation, getGeoLocation } from '@openpanel/geo';
|
||||
import { eventsQueue } from '@openpanel/queue';
|
||||
import { getLock } from '@openpanel/redis';
|
||||
import type {
|
||||
@@ -114,7 +114,7 @@ export async function handler(
|
||||
|
||||
switch (request.body.type) {
|
||||
case 'track': {
|
||||
const [salts, geo] = await Promise.all([getSalts(), parseIp(ip)]);
|
||||
const [salts, geo] = await Promise.all([getSalts(), getGeoLocation(ip)]);
|
||||
const currentDeviceId = ua
|
||||
? generateDeviceId({
|
||||
salt: salts.current,
|
||||
@@ -190,7 +190,7 @@ export async function handler(
|
||||
return;
|
||||
}
|
||||
|
||||
const geo = await parseIp(ip);
|
||||
const geo = await getGeoLocation(ip);
|
||||
await identify({
|
||||
payload: request.body.payload,
|
||||
projectId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getClientIp } from '@/utils/parse-ip';
|
||||
import { getClientIp } from '@/utils/get-client-ip';
|
||||
import type {
|
||||
FastifyReply,
|
||||
FastifyRequest,
|
||||
|
||||
@@ -239,5 +239,4 @@ const startServer = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// start
|
||||
startServer();
|
||||
|
||||
@@ -25,6 +25,12 @@ const miscRouter: FastifyPluginCallback = async (fastify) => {
|
||||
url: '/favicon/clear',
|
||||
handler: controller.clearFavicons,
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/geo',
|
||||
handler: controller.getGeo,
|
||||
});
|
||||
};
|
||||
|
||||
export default miscRouter;
|
||||
|
||||
8
apps/api/src/utils/get-client-ip.ts
Normal file
8
apps/api/src/utils/get-client-ip.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import requestIp from 'request-ip';
|
||||
|
||||
const ignore = ['127.0.0.1', '::1'];
|
||||
|
||||
export function getClientIp(req: FastifyRequest) {
|
||||
return requestIp.getClientIp(req);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { getRedisCache } from '@openpanel/redis';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import requestIp from 'request-ip';
|
||||
import { logger } from './logger';
|
||||
|
||||
interface RemoteIpLookupResponse {
|
||||
country: string | undefined;
|
||||
city: string | undefined;
|
||||
stateprov: string | undefined;
|
||||
longitude: number | undefined;
|
||||
latitude: number | undefined;
|
||||
}
|
||||
|
||||
export interface GeoLocation {
|
||||
country: string | undefined;
|
||||
city: string | undefined;
|
||||
region: string | undefined;
|
||||
longitude: number | undefined;
|
||||
latitude: number | undefined;
|
||||
}
|
||||
|
||||
const DEFAULT_GEO: GeoLocation = {
|
||||
country: undefined,
|
||||
city: undefined,
|
||||
region: undefined,
|
||||
longitude: undefined,
|
||||
latitude: undefined,
|
||||
};
|
||||
|
||||
const ignore = ['127.0.0.1', '::1'];
|
||||
|
||||
export function getClientIp(req: FastifyRequest) {
|
||||
return requestIp.getClientIp(req);
|
||||
}
|
||||
|
||||
export async function parseIp(ip?: string): Promise<GeoLocation> {
|
||||
if (!ip || ignore.includes(ip)) {
|
||||
return DEFAULT_GEO;
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha256').update(ip).digest('hex');
|
||||
const cached = await getRedisCache()
|
||||
.get(`geo:${hash}`)
|
||||
.catch(() => {
|
||||
logger.warn('Failed to get geo location from cache', { hash });
|
||||
return null;
|
||||
});
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${process.env.GEO_IP_HOST}/${ip}`, {
|
||||
signal: AbortSignal.timeout(4000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return DEFAULT_GEO;
|
||||
}
|
||||
|
||||
const json = (await res.json()) as RemoteIpLookupResponse;
|
||||
|
||||
const geo = {
|
||||
country: json.country,
|
||||
city: json.city,
|
||||
region: json.stateprov,
|
||||
longitude: json.longitude,
|
||||
latitude: json.latitude,
|
||||
};
|
||||
|
||||
await getRedisCache().set(
|
||||
`geo:${hash}`,
|
||||
JSON.stringify(geo),
|
||||
'EX',
|
||||
60 * 60 * 24,
|
||||
);
|
||||
|
||||
return geo;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch geo location for ip', { error });
|
||||
return DEFAULT_GEO;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user