Hey is a decentralized and permissionless social media app built with Lens Protocol 🌿
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: replace logger with prefixed logger in API files

yoginth.com e2179354 a8971ecf

verified
+95 -43
+40
AGENTS.md
··· 1 + # Repository Guidelines 2 + 3 + ## Project Structure & Modules 4 + - `apps/web`: Vite + React 19 frontend (sources under `src/`, static assets in `public/`). 5 + - `apps/api`: Hono-based API server (entry `src/index.ts`, routes in `src/routes/`). 6 + - `packages/*`: Shared code (`helpers`, `data`, `types`, `indexer`, `config`). 7 + - `script/*`: Maintenance utilities (e.g., sorting `package.json`, cleaning branches). 8 + - Tooling: PNPM workspaces (`pnpm-workspace.yaml`), Biome config (`biome.json`), Husky hooks (`.husky/`). 9 + 10 + ## Build, Test, and Development 11 + - Root dev: `pnpm dev` — run all workspaces in watch mode. 12 + - Root build: `pnpm build` — build all workspaces in parallel. 13 + - Web app: `pnpm -F @hey/web dev` (preview: `pnpm -F @hey/web start`, build: `pnpm -F @hey/web build`). 14 + - API: `pnpm -F @hey/api dev` (typecheck: `pnpm -F @hey/api typecheck`). 15 + - Lint/format: `pnpm biome:check` (auto-fix: `pnpm biome:fix`). 16 + - Types: `pnpm typecheck` — TypeScript across the monorepo. 17 + - Node & PM: Node 20 (`.nvmrc`), PNPM 10 (see `package.json#packageManager`). 18 + 19 + ## Coding Style & Naming 20 + - Language: TypeScript (strict, shared configs in `packages/config`). 21 + - Formatting: Biome controls style; no trailing commas; spaces for indentation. 22 + - Imports: Use workspace packages (`@hey/*`) and web alias `@/*` to `apps/web/src`. 23 + - Files: React components `PascalCase.tsx`; helpers/stores `camelCase.ts`. 24 + - Keep modules small, colocate domain helpers with their feature when practical. 25 + 26 + ## Testing Guidelines 27 + - Current status: no formal unit tests present. Enforce quality via `biome` and `tsc`. 28 + - If adding tests, prefer Vitest for web and lightweight integration tests for API. 29 + - Naming: `*.test.ts` or `*.test.tsx`, colocated with the code or under `__tests__/`. 30 + - Run with a future `pnpm test` script at root or per package. 31 + 32 + ## Commit & Pull Requests 33 + - Commits: imperative mood, concise subject; optional scope like `web:`, `api:`, `helpers:`. 34 + - Include rationale and references (e.g., `Closes #123`). 35 + - PRs: clear description, screenshots for UI changes, reproduction steps for fixes, and env notes. 36 + - CI hooks: pre-commit runs `biome` and type checks; ensure both pass locally before pushing. 37 + 38 + ## Security & Configuration 39 + - Copy `.env.example` to `.env` in `apps/web` and `apps/api`. Never commit secrets. 40 + - Validate envs at startup; keep keys minimal and documented near usage.
+5 -7
apps/api/src/index.ts
··· 1 1 import { serve } from "@hono/node-server"; 2 2 import "dotenv/config"; 3 3 import { Status } from "@hey/data/enums"; 4 - import logger from "@hey/helpers/logger"; 4 + import { withPrefix } from "@hey/helpers/logger"; 5 5 import { Hono } from "hono"; 6 6 import authContext from "./context/authContext"; 7 7 import authMiddleware from "./middlewares/authMiddleware"; ··· 17 17 import ping from "./routes/ping"; 18 18 import startDiscordWebhookWorker from "./workers/discordWebhook"; 19 19 20 + const log = withPrefix("[API]"); 21 + 20 22 const app = new Hono(); 21 23 22 - // Context 23 24 app.use(cors); 24 25 app.use(authContext); 25 26 26 - // Routes 27 27 app.get("/ping", ping); 28 28 app.route("/lens", lensRouter); 29 29 app.route("/cron", cronRouter); ··· 38 38 ); 39 39 40 40 serve({ fetch: app.fetch, port: 4784 }, (info) => { 41 - logger.info(`Server running on port ${info.port}`); 41 + log.info(`Server running on port ${info.port}`); 42 42 }); 43 43 44 - // Start Discord webhook worker if Redis + webhook envs are configured 45 44 if ( 46 45 process.env.REDIS_URL && 47 46 (process.env.EVENTS_DISCORD_WEBHOOK_URL || 48 47 process.env.PAGEVIEWS_DISCORD_WEBHOOK_URL) 49 48 ) { 50 - // Fire and forget 51 49 void startDiscordWebhookWorker(); 52 50 } else { 53 - logger.warn("Discord worker not started: missing Redis or webhook envs"); 51 + log.warn("Discord worker not started: missing Redis or webhook envs"); 54 52 }
+4 -4
apps/api/src/middlewares/authMiddleware.ts
··· 1 1 import { LENS_API_URL } from "@hey/data/constants"; 2 - import logger from "@hey/helpers/logger"; 2 + import { withPrefix } from "@hey/helpers/logger"; 3 3 import type { Context, Next } from "hono"; 4 4 import { createRemoteJWKSet, jwtVerify } from "jose"; 5 5 6 6 const jwksUri = `${LENS_API_URL.replace("/graphql", "")}/.well-known/jwks.json`; 7 - // Cache the JWKS for 12 hours 8 7 const JWKS = createRemoteJWKSet(new URL(jwksUri), { 9 8 cacheMaxAge: 60 * 60 * 12 10 9 }); ··· 12 11 const unauthorized = (c: Context) => c.body("Unauthorized", 401); 13 12 14 13 const authMiddleware = async (c: Context, next: Next) => { 14 + const log = withPrefix("[API]"); 15 15 const token = c.get("token"); 16 16 17 17 if (!token) { 18 - logger.warn("missing token"); 18 + log.warn("missing token"); 19 19 return unauthorized(c); 20 20 } 21 21 22 22 try { 23 23 await jwtVerify(token, JWKS); 24 24 } catch { 25 - logger.warn("invalid token"); 25 + log.warn("invalid token"); 26 26 return unauthorized(c); 27 27 } 28 28
+4 -2
apps/api/src/pageview.ts
··· 1 1 import { Status } from "@hey/data/enums"; 2 - import logger from "@hey/helpers/logger"; 2 + import { withPrefix } from "@hey/helpers/logger"; 3 3 import type { Context } from "hono"; 4 4 import enqueueDiscordWebhook from "./utils/discordQueue"; 5 5 import getIpData from "./utils/getIpData"; 6 + 7 + const log = withPrefix("[API]"); 6 8 7 9 interface PageviewBody { 8 10 path?: string; ··· 61 63 }; 62 64 await enqueueDiscordWebhook(item); 63 65 } catch (err) { 64 - logger.error("Failed to enqueue pageview webhook", err as Error); 66 + log.error("Failed to enqueue pageview webhook", err as Error); 65 67 } 66 68 67 69 return ctx.json({ data: { ok: true }, status: Status.Success });
+4 -2
apps/api/src/posts.ts
··· 1 1 import { Status } from "@hey/data/enums"; 2 - import logger from "@hey/helpers/logger"; 2 + import { withPrefix } from "@hey/helpers/logger"; 3 3 import type { Context } from "hono"; 4 4 import enqueueDiscordWebhook from "./utils/discordQueue"; 5 + 6 + const log = withPrefix("[API]"); 5 7 6 8 interface PostsBody { 7 9 slug?: string; ··· 35 37 36 38 await enqueueDiscordWebhook(item); 37 39 } catch (err) { 38 - logger.error("Failed to enqueue post webhook", err as Error); 40 + log.error("Failed to enqueue post webhook", err as Error); 39 41 } 40 42 41 43 return ctx.json({ data: { ok: true }, status: Status.Success });
+4 -4
apps/api/src/routes/cron/removeExpiredSubscribers/index.ts
··· 1 1 import { PERMISSIONS } from "@hey/data/constants"; 2 2 import { Status } from "@hey/data/enums"; 3 - import logger from "@hey/helpers/logger"; 3 + import { withPrefix } from "@hey/helpers/logger"; 4 4 import type { Context } from "hono"; 5 5 import handleApiError from "@/utils/handleApiError"; 6 6 import lensPg from "@/utils/lensPg"; ··· 31 31 }); 32 32 } 33 33 34 - // Run the removal operation in the background without awaiting 34 + const log = withPrefix("[API]"); 35 35 const membersToRemove = addresses.map((addr) => ({ 36 36 account: addr, 37 37 customParams: [], ··· 46 46 functionName: "removeMembers" 47 47 }) 48 48 .then((hash) => { 49 - logger.info("Expired subscribers removal completed", { 49 + log.info("Expired subscribers removal completed", { 50 50 count: addresses.length, 51 51 hash 52 52 }); 53 53 }) 54 54 .catch((error) => { 55 - logger.error("Expired subscribers removal failed:", error); 55 + log.error("Expired subscribers removal failed:", error); 56 56 }); 57 57 58 58 return ctx.json({
+4 -3
apps/api/src/utils/discordQueue.ts
··· 1 - import logger from "@hey/helpers/logger"; 1 + import { withPrefix } from "@hey/helpers/logger"; 2 2 import { DISCORD_QUEUE_KEY, getRedis } from "./redis"; 3 3 4 4 export interface DiscordQueueItemBase { ··· 21 21 export const enqueueDiscordWebhook = async ( 22 22 item: DiscordQueueItem 23 23 ): Promise<void> => { 24 + const log = withPrefix("[API]"); 24 25 try { 25 26 const redis = getRedis(); 26 27 await redis.rpush(DISCORD_QUEUE_KEY, JSON.stringify(item)); 27 - logger.info(`Enqueued discord webhook: ${item.kind}`); 28 + log.info(`Enqueued discord webhook: ${item.kind}`); 28 29 } catch (err) { 29 - logger.error("Failed to enqueue discord webhook", err as Error); 30 + log.error("Failed to enqueue discord webhook", err as Error); 30 31 } 31 32 }; 32 33
+3 -2
apps/api/src/utils/handleApiError.ts
··· 1 1 import { Status } from "@hey/data/enums"; 2 2 import { ERRORS } from "@hey/data/errors"; 3 - import logger from "@hey/helpers/logger"; 3 + import { withPrefix } from "@hey/helpers/logger"; 4 4 import type { Context } from "hono"; 5 5 import ApiError from "@/utils/apiError"; 6 6 7 7 const handleApiError = (ctx: Context, error?: unknown): Response => { 8 - logger.error(error); 8 + const log = withPrefix("[API]"); 9 + log.error(error); 9 10 10 11 if (error instanceof ApiError) { 11 12 ctx.status(error.statusCode);
+3 -2
apps/api/src/utils/lensPg.ts
··· 1 - import logger from "@hey/helpers/logger"; 1 + import { withPrefix } from "@hey/helpers/logger"; 2 2 import dotenv from "dotenv"; 3 3 import type { IDatabase, IFormatting, IHelpers, IMain } from "pg-promise"; 4 4 import pgPromise from "pg-promise"; ··· 44 44 ): InitializeDbResult { 45 45 const pgp = pgPromise({ 46 46 error: (error: unknown) => { 47 + const log = withPrefix("[API]"); 47 48 const errorMessage = 48 49 error instanceof Error ? error.message : String(error); 49 - logger.error(`LENS POSTGRES ERROR WITH TRACE: ${errorMessage}`); 50 + log.error(`LENS POSTGRES ERROR WITH TRACE: ${errorMessage}`); 50 51 } 51 52 }); 52 53
-2
apps/api/src/utils/redis.ts
··· 15 15 redisClient = new IORedis(url); 16 16 return redisClient; 17 17 }; 18 - 19 - export default getRedis;
+4 -4
apps/api/src/utils/syncAddressesToGuild.ts
··· 1 1 import { createGuildClient, createSigner } from "@guildxyz/sdk"; 2 2 import { Status } from "@hey/data/enums"; 3 - import logger from "@hey/helpers/logger"; 3 + import { withPrefix } from "@hey/helpers/logger"; 4 4 import signer from "./signer"; 5 5 6 6 const guildClient = createGuildClient("heyxyz"); ··· 23 23 requirementId: number; 24 24 roleId: number; 25 25 }) => { 26 - // Run the sync operation in the background without awaiting 26 + const log = withPrefix("[API]"); 27 27 requirementClient 28 28 .update( 29 29 7465, ··· 33 33 signerFunction 34 34 ) 35 35 .then(() => { 36 - logger.info("Guild sync completed"); 36 + log.info("Guild sync completed"); 37 37 }) 38 38 .catch((error) => { 39 - logger.error("Guild sync failed:", error); 39 + log.error("Guild sync failed:", error); 40 40 }); 41 41 42 42 return {
+11 -11
apps/api/src/workers/discordWebhook.ts
··· 1 - import logger from "@hey/helpers/logger"; 1 + import { withPrefix } from "@hey/helpers/logger"; 2 2 import type IORedis from "ioredis"; 3 3 import type { 4 4 DiscordQueueItem, ··· 6 6 PostQueueItem 7 7 } from "../utils/discordQueue"; 8 8 import { DISCORD_QUEUE_KEY, getRedis } from "../utils/redis"; 9 + 10 + const log = withPrefix("[Worker]"); 9 11 10 12 const sleep = (ms: number): Promise<void> => 11 13 new Promise((res) => setTimeout(res, ms)); ··· 20 22 if (!item || !("kind" in item)) return null; 21 23 return item; 22 24 } catch (e) { 23 - logger.error("Failed to parse queue item", e as Error); 25 + log.error("Failed to parse queue item", e as Error); 24 26 return null; 25 27 } 26 28 }; ··· 44 46 } 45 47 46 48 if (!webhookUrl) { 47 - logger.warn(`Skipping ${item.kind} webhook: missing webhook URL env`); 49 + log.warn(`Skipping ${item.kind} webhook: missing webhook URL env`); 48 50 return; 49 51 } 50 52 ··· 74 76 try { 75 77 redis = getRedis(); 76 78 } catch (_e) { 77 - logger.warn("Discord worker disabled: Redis not configured"); 79 + log.warn("Discord worker disabled: Redis not configured"); 78 80 return; 79 81 } 80 82 81 - logger.info(`Discord worker started. Queue: ${DISCORD_QUEUE_KEY}`); 83 + log.info(`Discord worker started. Queue: ${DISCORD_QUEUE_KEY}`); 82 84 83 85 // Blocking pop loop using BRPOP with small timeout 84 86 // eslint-disable-next-line no-constant-condition ··· 95 97 96 98 try { 97 99 await dispatch(item); 98 - logger.info(`Dispatched Discord webhook: ${item.kind}`); 100 + log.info(`Dispatched Discord webhook: ${item.kind}`); 99 101 } catch (_err) { 100 102 const retries = (item.retries ?? 0) + 1; 101 103 if (retries <= 3) { 102 104 item.retries = retries; 103 105 await redis.rpush(DISCORD_QUEUE_KEY, JSON.stringify(item)); 104 - logger.warn(`Requeued ${item.kind} webhook (attempt ${retries})`); 106 + log.warn(`Requeued ${item.kind} webhook (attempt ${retries})`); 105 107 } else { 106 - logger.error( 107 - `Dropped ${item.kind} webhook after ${retries} attempts` 108 - ); 108 + log.error(`Dropped ${item.kind} webhook after ${retries} attempts`); 109 109 } 110 110 } 111 111 } catch (loopErr) { 112 - logger.error("Discord worker loop error", loopErr as Error); 112 + log.error("Discord worker loop error", loopErr as Error); 113 113 await sleep(1000); 114 114 } 115 115 }
+9
packages/helpers/logger.ts
··· 16 16 } 17 17 }; 18 18 19 + export const withPrefix = (prefix: string) => { 20 + return { 21 + debug: (...args: unknown[]) => debug(prefix, ...args), 22 + error: (...args: unknown[]) => error(prefix, ...args), 23 + info: (...args: unknown[]) => info(prefix, ...args), 24 + warn: (...args: unknown[]) => warn(prefix, ...args) 25 + }; 26 + }; 27 + 19 28 export default { 20 29 debug, 21 30 error,