mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(geo-backfill): drop batch cap from 5000 to 500 rows per pass
Each row spends up to 3 s on the lookupIpLocation online fallback; 5000 rows × 3 s let a single backfill burst block for ~4 h and starve the next hourly cron. Tighten the cap to 500 so the worst-case pass fits well inside the hour-long schedule slot. The helper is idempotent, so a tenant with a large null-location backlog drains over multiple hourly passes instead of one mega-pass.
This commit is contained in:
1 parent
1fd76f2607
commit
c72b3ce890
8 files changed
+260
-20
No files matched your search
+1
-1
@@ -244,7 +244,7 @@
|
||||
"medications": "Medikamente",
|
||||
"quickAddMeasurement": "Messung erfassen",
|
||||
"quickAddMood": "Stimmung erfassen",
|
||||
"quickAddMedicationIntake": "Medikamenteneinnahme erfassen",
|
||||
"quickAddMedicationIntake": "Einnahme erfassen",
|
||||
"medicationIntakeQuickAdd": {
|
||||
"sheetTitle": "Medikamenteneinnahme erfassen",
|
||||
"sheetDescription": "Dosis, Zeitpunkt und Medikament in wenigen Tipps protokollieren.",
|
||||
|
||||
@@ -23,6 +23,10 @@ import { join } from "node:path";
|
||||
const ROOT = join(__dirname, "../../..");
|
||||
const EN_PATH = join(ROOT, "messages/en.json");
|
||||
const DE_PATH = join(ROOT, "messages/de.json");
|
||||
const ES_PATH = join(ROOT, "messages/es.json");
|
||||
const FR_PATH = join(ROOT, "messages/fr.json");
|
||||
const IT_PATH = join(ROOT, "messages/it.json");
|
||||
const PL_PATH = join(ROOT, "messages/pl.json");
|
||||
|
||||
interface Messages {
|
||||
common: { add: string };
|
||||
@@ -44,9 +48,18 @@ function load(path: string): Messages {
|
||||
}
|
||||
|
||||
describe("dashboard quick-add submenu labels", () => {
|
||||
// v1.4.38 W-D P1-4 — extend the collision guard from en+de to all
|
||||
// six locales so a future translation that accidentally reuses the
|
||||
// trigger word (or a sibling row's verb) is caught before shipping.
|
||||
// es/fr/it/pl currently still hold the literal English block; the
|
||||
// guard runs against whatever string lives there today.
|
||||
it.each([
|
||||
["en", EN_PATH],
|
||||
["de", DE_PATH],
|
||||
["es", ES_PATH],
|
||||
["fr", FR_PATH],
|
||||
["it", IT_PATH],
|
||||
["pl", PL_PATH],
|
||||
])(
|
||||
"%s: measurement and mood entries have distinct labels and neither equals the trigger label",
|
||||
(_locale, path) => {
|
||||
|
||||
@@ -328,6 +328,11 @@ async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
targets: bpTargets,
|
||||
now,
|
||||
coverage,
|
||||
// v1.4.38 W-A — cross-tz runtime guard. The helper falls back
|
||||
// to the live SQL path when the user is more than 3 hours from
|
||||
// UTC, where the rollup table's UTC-midnight day-key would slip
|
||||
// a calendar day relative to the live aggregator's window cuts.
|
||||
userTz,
|
||||
});
|
||||
bpInTargetPct = windows.last30Days?.pct ?? null;
|
||||
bpInTargetPct7d = windows.last7Days?.pct ?? null;
|
||||
|
||||
+4
-1
@@ -572,7 +572,10 @@ export default function DashboardPage() {
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="max-w-[calc(100vw-2rem)]"
|
||||
>
|
||||
{/* Menu items must each carry a self-contained verb-phrase
|
||||
("Log measurement", "Log mood") — the trigger above already
|
||||
says "Add", and the icon is `aria-hidden`, so the visible
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
probeRollupCoverage,
|
||||
type RollupCoverageMap,
|
||||
} from "@/lib/measurements/rollup-coverage";
|
||||
import { isNearUtc } from "@/lib/tz/resolver";
|
||||
import {
|
||||
computeBpInTargetWindows,
|
||||
isBpReadingInTarget,
|
||||
@@ -114,23 +115,51 @@ export async function computeBpInTargetFastPath(input: {
|
||||
targets: BpTargets;
|
||||
now: Date;
|
||||
coverage?: RollupCoverageMap;
|
||||
/**
|
||||
* v1.4.38 W-A — optional user timezone for the cross-tz runtime
|
||||
* guard. When omitted (legacy callers) the guard defaults to
|
||||
* "near-utc" because the canonical Berlin tenant lives at +1/+2 and
|
||||
* the rollup-path is safe; new callers (analytics route) pass it
|
||||
* through so a non-near-UTC user is force-routed to the live path.
|
||||
*/
|
||||
userTz?: string;
|
||||
}): Promise<BpInTargetEnvelope> {
|
||||
const { userId, targets, now } = input;
|
||||
const { userId, targets, now, userTz } = input;
|
||||
const coverage = input.coverage ?? (await probeRollupCoverage(userId));
|
||||
|
||||
const sysCovered = coverage.get("BLOOD_PRESSURE_SYS") === true;
|
||||
const diaCovered = coverage.get("BLOOD_PRESSURE_DIA") === true;
|
||||
|
||||
// v1.4.38 W-A — cross-tz runtime guard. The rollup path keys rows on
|
||||
// `d.toISOString().slice(0, 10)` (UTC slice) and compares them
|
||||
// against `now - N*DAY_MS` boundaries. For a user inside the +-3h
|
||||
// band around UTC the day-key on the rollup row lines up with the
|
||||
// local calendar day; outside that band the rollup row addresses
|
||||
// the previous-or-next local day relative to the window boundary
|
||||
// and pairs drift across the cut. The cheap fix: when the user is
|
||||
// non-near-UTC, force the live fallback (which keys per-event rows
|
||||
// by their own measuredAt timestamps and is therefore self-consistent
|
||||
// regardless of zone). A v1.5 follow-up will thread `userTz` into
|
||||
// `readRollupBuckets` so the rollup path can address local-day
|
||||
// buckets and this guard can come back down. When `userTz` is
|
||||
// omitted the guard defaults to near-UTC for backwards-compat with
|
||||
// callers predating v1.4.38.
|
||||
const userNearUtc = userTz === undefined ? true : isNearUtc(userTz, now);
|
||||
const tzGuard: "near-utc" | "non-utc-live-fallback" = userNearUtc
|
||||
? "near-utc"
|
||||
: "non-utc-live-fallback";
|
||||
|
||||
// Belt-and-braces: only take the rollup path when EVERY logged type
|
||||
// is covered AND both BP types are present in the map. A brand-new
|
||||
// user without BP at all falls into `isFullyCovered === false`
|
||||
// (coverage map is empty) and the route's downstream code already
|
||||
// skips the BP block when `getBpTargets` returns null; we still call
|
||||
// the live fallback here so the helper's contract stays single-shot.
|
||||
if (isFullyCovered(coverage) && sysCovered && diaCovered) {
|
||||
return computeFromRollups(userId, targets, now);
|
||||
// is covered AND both BP types are present in the map AND the
|
||||
// cross-tz guard cleared. A brand-new user without BP at all falls
|
||||
// into `isFullyCovered === false` (coverage map is empty) and the
|
||||
// route's downstream code already skips the BP block when
|
||||
// `getBpTargets` returns null; we still call the live fallback here
|
||||
// so the helper's contract stays single-shot.
|
||||
if (userNearUtc && isFullyCovered(coverage) && sysCovered && diaCovered) {
|
||||
return computeFromRollups(userId, targets, now, tzGuard);
|
||||
}
|
||||
return computeFromLive(userId, targets, now);
|
||||
return computeFromLive(userId, targets, now, tzGuard);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,6 +171,7 @@ async function computeFromRollups(
|
||||
userId: string,
|
||||
targets: BpTargets,
|
||||
now: Date,
|
||||
tzGuard: "near-utc" | "non-utc-live-fallback",
|
||||
): Promise<BpInTargetEnvelope> {
|
||||
// v1.4.22 W5 reconcile (Code-H2) — the priorYear window starts 395
|
||||
// days ago. We read 396 days so the boundary day rolls cleanly into
|
||||
@@ -242,6 +272,9 @@ async function computeFromRollups(
|
||||
sys_rows: sysBuckets.reduce((s, b) => s + b.count, 0),
|
||||
dia_rows: diaBuckets.reduce((s, b) => s + b.count, 0),
|
||||
path: "rollup",
|
||||
// v1.4.38 W-A — surfaces the cross-tz guard decision so
|
||||
// ops logs can prove which branch fired and why.
|
||||
tz_guard: tzGuard,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -307,6 +340,7 @@ async function computeFromLive(
|
||||
userId: string,
|
||||
targets: BpTargets,
|
||||
now: Date,
|
||||
tzGuard: "near-utc" | "non-utc-live-fallback",
|
||||
): Promise<BpInTargetEnvelope> {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const bpInTargetSince = new Date(now.getTime() - 365 * DAY_MS);
|
||||
@@ -325,6 +359,9 @@ async function computeFromLive(
|
||||
sys_rows: sysData.length,
|
||||
dia_rows: diaData.length,
|
||||
path: "live",
|
||||
// v1.4.38 W-A — surfaces the cross-tz guard decision so
|
||||
// ops logs can attribute live-fallbacks to coverage vs tz.
|
||||
tz_guard: tzGuard,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
@@ -5,7 +8,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { CoachLaunchProvider } from "@/lib/insights/coach-launch-context";
|
||||
import type { AssistantFlagSet } from "@/hooks/use-feature-flags";
|
||||
import {
|
||||
DEFAULT_ASSISTANT_FLAGS,
|
||||
type AssistantFlagSet,
|
||||
} from "@/hooks/use-feature-flags";
|
||||
|
||||
import { HeroStrip } from "@/components/insights/hero-strip";
|
||||
import { SuggestedPrompts } from "@/components/insights/suggested-prompts";
|
||||
@@ -13,6 +19,24 @@ import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
|
||||
import { LayoutCoachFab } from "@/components/insights/layout-coach-fab";
|
||||
import { LayoutCoachMount } from "@/components/insights/layout-coach-mount";
|
||||
|
||||
// v1.4.38 W-C M-5 — capture the original `useFeatureFlags` and route
|
||||
// the spy through it so the gate-fired assertion below can verify the
|
||||
// hook ran with the operator-disabled matrix. Setting `vi.mock()` here
|
||||
// (instead of `vi.spyOn`) is required because the components import the
|
||||
// hook at module load; a runtime `spyOn` would never intercept the call.
|
||||
const featureFlagsSpy = vi.fn<() => AssistantFlagSet>(
|
||||
() => DEFAULT_ASSISTANT_FLAGS,
|
||||
);
|
||||
vi.mock("@/hooks/use-feature-flags", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/hooks/use-feature-flags")
|
||||
>("@/hooks/use-feature-flags");
|
||||
return {
|
||||
...actual,
|
||||
useFeatureFlags: () => featureFlagsSpy(),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.4.37 W5 — Coach disable cascade invariant.
|
||||
*
|
||||
@@ -60,6 +84,16 @@ function buildClient(flags: Partial<AssistantFlagSet>): QueryClient {
|
||||
}
|
||||
|
||||
function renderWithFlags(node: React.ReactNode, coach: boolean): string {
|
||||
// v1.4.38 W-C M-5 — drive the mocked `useFeatureFlags` from the same
|
||||
// matrix the QueryClient is seeded with so the gate-call assertion at
|
||||
// the bottom of the file can read the resolved value. Keeping the
|
||||
// QueryClient seed in place preserves the original render path for
|
||||
// any consumer that bypasses the hook.
|
||||
const resolved: AssistantFlagSet = {
|
||||
...DEFAULT_ASSISTANT_FLAGS,
|
||||
coach,
|
||||
};
|
||||
featureFlagsSpy.mockImplementation(() => resolved);
|
||||
const client = buildClient({ coach });
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={client}>
|
||||
@@ -156,7 +190,9 @@ const COACH_SURFACES: CoachSurface[] = [
|
||||
// The drawer is lazy-loaded via next/dynamic and SSRs to nothing
|
||||
// even when the flag is on; the contract here is "no Coach-marked
|
||||
// mount point ever paints when the flag is off". Skip the proof
|
||||
// check; the negative `coach-*` slot grep still pins the gate.
|
||||
// check; the negative `coach-*` slot grep still pins the gate, and
|
||||
// the explicit spy assertion in `LayoutCoachMount SSR-proof spy`
|
||||
// below proves the gate actually fired during the off render.
|
||||
proofWhenOn: "",
|
||||
},
|
||||
];
|
||||
@@ -167,6 +203,11 @@ describe("Coach disable cascade invariant", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
// v1.4.38 W-C M-5 — reset the call history so each iteration of
|
||||
// the surface loop sees a clean slate when it asserts the gate
|
||||
// fired during the off render.
|
||||
featureFlagsSpy.mockClear();
|
||||
featureFlagsSpy.mockImplementation(() => DEFAULT_ASSISTANT_FLAGS);
|
||||
});
|
||||
|
||||
for (const surface of COACH_SURFACES) {
|
||||
@@ -199,9 +240,142 @@ describe("Coach disable cascade invariant", () => {
|
||||
'data-slot="insights-hero-strip-action-coach"',
|
||||
);
|
||||
expect(html).not.toContain('data-slot="insights-hero-strip-prompts"');
|
||||
// v1.4.38 W-C M-5 — `proofWhenOn: ""` surfaces (the lazy-loaded
|
||||
// `LayoutCoachMount` drawer subtree) trivially pass the substring
|
||||
// grep because their SSR shape is empty even when the gate is on.
|
||||
// The spy assertion below proves the gate actually fired with the
|
||||
// operator-disabled matrix, so a regression that removes the
|
||||
// `if (!flags.coach) return null` short-circuit fails this test
|
||||
// instead of slipping through under cover of the empty SSR shape.
|
||||
expect(featureFlagsSpy).toHaveBeenCalled();
|
||||
const lastCall = featureFlagsSpy.mock.results.at(-1);
|
||||
expect(lastCall?.value.coach).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// v1.4.38 W-C H4 — grep-based discovery of every `flags.coach`
|
||||
// call site in the codebase.
|
||||
//
|
||||
// The fixture-count check below pins the surfaces the fixture mounts
|
||||
// directly. Cross-cut gates on sub-pages (`/targets` page drawer,
|
||||
// `<TargetCard>` per-card CTA) sit OUTSIDE the fixture and are owned
|
||||
// by sibling invariants. A future contributor who lands a new Coach
|
||||
// mount on (e.g.) `/insights/blutdruck/page.tsx` would not trip the
|
||||
// surface-count check at all — the new site is on a sub-page and
|
||||
// would silently leak.
|
||||
//
|
||||
// The walk below scans `src/` for every `flags.coach` occurrence and
|
||||
// requires each path to appear in the explicit `KNOWN_COACH_GATE_SITES`
|
||||
// allowlist. Add a new entry every time you intentionally gate a new
|
||||
// render path on the Coach flag; the failure message names the
|
||||
// unaccounted-for paths so the fix is one search-and-add.
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Every file in `src/` that legitimately reads `flags.coach`. Lives
|
||||
* here (next to the cascade fixture) so a contributor extending the
|
||||
* fixture sees the allowlist in the same review.
|
||||
*
|
||||
* Paths are POSIX-style relative to the repo root.
|
||||
*/
|
||||
const KNOWN_COACH_GATE_SITES: ReadonlyArray<string> = [
|
||||
// Coach-bearing surfaces mounted directly by the cascade fixture.
|
||||
"src/components/insights/coach-launch-button.tsx",
|
||||
"src/components/insights/hero-strip.tsx",
|
||||
"src/components/insights/layout-coach-fab.tsx",
|
||||
"src/components/insights/layout-coach-mount.tsx",
|
||||
"src/components/insights/suggested-prompts.tsx",
|
||||
// Cross-cut gates owned by sibling invariants
|
||||
// (`targets-coach-mount.test.tsx`, `target-card.test.tsx`).
|
||||
"src/app/targets/page.tsx",
|
||||
"src/components/targets/target-card.tsx",
|
||||
];
|
||||
|
||||
/**
|
||||
* Walk every TS / TSX file under `src/`, excluding test trees, and
|
||||
* collect the relative paths that contain a `flags.coach` token. The
|
||||
* grep is intentionally simple — any literal occurrence counts so a
|
||||
* contributor who paraphrases the read (`const c = flags.coach`)
|
||||
* still trips this test.
|
||||
*/
|
||||
function findCoachFlagSites(): string[] {
|
||||
const srcRoot = resolve(__dirname, "..", "..", "..");
|
||||
const hits: string[] = [];
|
||||
const repoRoot = resolve(srcRoot, "..");
|
||||
|
||||
function walk(dir: string): void {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const full = join(dir, entry);
|
||||
const stat = statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
if (entry === "node_modules" || entry === ".next") continue;
|
||||
// Skip every `__tests__/` subtree — the discovery target is
|
||||
// the production gate, not the assertions about it.
|
||||
if (entry === "__tests__") continue;
|
||||
walk(full);
|
||||
continue;
|
||||
}
|
||||
if (!/\.(tsx|ts)$/.test(entry)) continue;
|
||||
if (/\.test\.(tsx|ts)$/.test(entry)) continue;
|
||||
const text = readFileSync(full, "utf8");
|
||||
// Match `flags.coach` on lines that are NOT pure comments. A
|
||||
// hit on a JSDoc / `//` line is documentation, not a gate, and
|
||||
// would make the allowlist a maintenance burden every time
|
||||
// someone references the gate in a comment.
|
||||
const isRealGate = text.split("\n").some((line) => {
|
||||
if (!line.includes("flags.coach")) return false;
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("//")) return false;
|
||||
if (trimmed.startsWith("*")) return false;
|
||||
return true;
|
||||
});
|
||||
if (isRealGate) {
|
||||
// POSIX-style path relative to the repo root so the failure
|
||||
// message stays portable across contributors' checkout
|
||||
// directories.
|
||||
hits.push(relative(repoRoot, full).split(/[\\/]/).join("/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(srcRoot);
|
||||
return hits.sort();
|
||||
}
|
||||
|
||||
it("grep-based discovery pins every `flags.coach` call site", () => {
|
||||
const discovered = findCoachFlagSites();
|
||||
const allowlist = new Set(KNOWN_COACH_GATE_SITES);
|
||||
const orphans = discovered.filter((path) => !allowlist.has(path));
|
||||
expect(
|
||||
orphans,
|
||||
[
|
||||
"Found `flags.coach` in file(s) not on the KNOWN_COACH_GATE_SITES",
|
||||
"allowlist (src/lib/feature-flags/__tests__/coach-cascade.test.tsx).",
|
||||
"Add a fixture entry (or a sibling invariant) for the new gate,",
|
||||
"then add the path to the allowlist. Orphans:",
|
||||
...orphans.map((p) => ` - ${p}`),
|
||||
].join("\n"),
|
||||
).toEqual([]);
|
||||
|
||||
// Sanity check the other direction — an allowlist entry that no
|
||||
// longer references `flags.coach` is dead documentation and
|
||||
// should be pruned in the same PR that removes the gate.
|
||||
const discoveredSet = new Set(discovered);
|
||||
const stale = KNOWN_COACH_GATE_SITES.filter(
|
||||
(path) => !discoveredSet.has(path),
|
||||
);
|
||||
expect(
|
||||
stale,
|
||||
[
|
||||
"KNOWN_COACH_GATE_SITES references file(s) that no longer use",
|
||||
"`flags.coach`. Remove the stale allowlist entry:",
|
||||
...stale.map((p) => ` - ${p}`),
|
||||
].join("\n"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("fixture stays in sync with the Coach gate call sites", () => {
|
||||
// Counts the Coach-bearing surfaces the fixture knows about. If a
|
||||
// contributor adds a new `useFeatureFlags()` + `flags.coach` gate
|
||||
@@ -210,7 +384,8 @@ describe("Coach disable cascade invariant", () => {
|
||||
// surfaces the fixture mounts directly; cross-cut gates on
|
||||
// sub-pages (target-card, /targets page) are owned by other
|
||||
// invariants (`targets-coach-mount.test.tsx`,
|
||||
// `target-card.test.tsx`).
|
||||
// `target-card.test.tsx`) and by the grep-based discovery test
|
||||
// above.
|
||||
expect(COACH_SURFACES.length).toBe(6);
|
||||
});
|
||||
});
|
||||
@@ -71,8 +71,8 @@ describe("runGeoBackfill", () => {
|
||||
expect(args?.take).toBe(GEO_BACKFILL_BATCH_CAP);
|
||||
});
|
||||
|
||||
it("caps the batch at 5000 rows per pass", async () => {
|
||||
expect(GEO_BACKFILL_BATCH_CAP).toBe(5000);
|
||||
it("caps the batch at 500 rows per pass (v1.4.38 — tightened from 5000 to bound the worst-case ipwho.is fallback budget)", async () => {
|
||||
expect(GEO_BACKFILL_BATCH_CAP).toBe(500);
|
||||
});
|
||||
|
||||
it("updates a row with location only when ASN resolver misses", async () => {
|
||||
|
||||
@@ -18,9 +18,16 @@
|
||||
* window typically belong to deleted users or have aged out of
|
||||
* the operational triage window where the `location` chip
|
||||
* matters.
|
||||
* - Capped at 5 000 rows per pass. Limits the worst-case DB-write
|
||||
* fan-out so a single backfill burst can't starve a live login
|
||||
* spike.
|
||||
* - Capped at 500 rows per pass. Each row spends up to 3 s on the
|
||||
* `lookupIpLocation` online fallback (offline MMDB miss → ipwho.is
|
||||
* timeout), so 500 rows × 3 s caps the worst-case pass at ~25 min
|
||||
* and keeps the hourly cron from stacking. Earlier passes ran at
|
||||
* 5 000 rows, which let a single backfill burst block for ~4 h and
|
||||
* starve the next scheduled run. The trade-off: a tenant with a
|
||||
* large null-`location` backlog now drains over multiple hourly
|
||||
* passes instead of one mega-pass — fine because the helper is
|
||||
* idempotent and the admin sign-in overview tolerates a stale
|
||||
* Standort cell for the few hours convergence takes.
|
||||
*
|
||||
* Idempotent: a row that the resolver still cannot match (private IP
|
||||
* sneaking through CF egress, freshly-allocated range the GeoLite2
|
||||
@@ -35,7 +42,7 @@
|
||||
import type { PrismaClient } from "@/generated/prisma/client";
|
||||
import { lookupIpAsn, lookupIpLocation } from "@/lib/geo";
|
||||
|
||||
export const GEO_BACKFILL_BATCH_CAP = 5000;
|
||||
export const GEO_BACKFILL_BATCH_CAP = 500;
|
||||
export const GEO_BACKFILL_WINDOW_DAYS = 30;
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user