dev #71

Merged
root merged 11 commits from dev into main 2026-06-01 18:52:42 +03:00
12 changed files with 311 additions and 64 deletions

View File

@@ -936,7 +936,7 @@ export class CategoriesService {
// verification can measure source-DB coverage.
let partsResult = await this.pcatSourceDb.fetchParts(catalogId, carId, groupId);
if (partsResult) {
this.logger.debug(`[source-db hit pcat] car=${carId} group=${groupId}`);
this.logger.log(`[source-db hit pcat] car=${carId} group=${groupId}`);
} else {
partsResult = await this.partsCatalogsService.fetchParts(
catalogId,
@@ -1092,16 +1092,19 @@ export class CategoriesService {
} else if (vehicle && category.source === "emex") {
// EMEX: fetch parts + schema image via Puppeteer from QuickDetails URL
try {
// Local dump first (uses vehicles.rawData.ssd persisted by the
// emex mapper). Falls back to live upstream on miss.
const emexSsd = (vehicle.rawData as { ssd?: string } | null)?.ssd;
// Local dump first (catalog-wide via vehicles.rawData.catalogCode +
// category.externalId). The dump's per-vehicle bridge is the EMEX
// SSD, which is session-regenerated and never matches what sase
// stored — so we accept over-return at the catalog level (~71% hit).
// Falls back to live upstream on miss.
const catalogCode = (vehicle.rawData as { catalogCode?: string } | null)?.catalogCode;
let emexResult = await this.emexSourceDb.fetchCategoryParts(
emexSsd,
category.linkPath ?? "",
catalogCode,
category.externalId,
);
if (emexResult) {
this.logger.debug(
`[source-db hit emex] ssd=${emexSsd?.slice(0, 12)}... gid=${category.externalId}`,
this.logger.log(
`[source-db hit emex] catalog=${catalogCode} gid=${category.externalId}`,
);
} else {
emexResult = await this.emexService.fetchCategoryParts(category.linkPath);

View File

@@ -73,6 +73,17 @@ export default () => ({
enabled: process.env.CATALOG_SOURCE_DB_ENABLED === "true",
pcatUrl: process.env.PCAT_SOURCE_DB_URL,
emexUrl: process.env.EMEX_SOURCE_DB_URL,
// Per-source kill switches. PCAT defaults off — the current dump's deep-scrape
// doesn't cover sase's TR-market vehicles; see service comment for details.
emexEnabled: (process.env.EMEX_SOURCE_DB_ENABLED ?? "true") === "true",
pcatEnabled: process.env.PCAT_SOURCE_DB_ENABLED === "true",
// Per-catalog allowlist for emex parts lookup. Empty → service returns
// null for every catalog (safe default after the 2026-06-01 noise audit).
// Populate ONLY after a catalog's per-vehicle bridge is wired & verified.
emexAllowedCatalogs: (process.env.EMEX_SOURCE_DB_ALLOWED_CATALOGS ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
},
otel: {
enabled: process.env.OTEL_ENABLED === "true",

View File

@@ -4,31 +4,50 @@ import mysql, { type Pool, type RowDataPacket } from "mysql2/promise";
import type { EmexHotspot, EmexHotspotArea, EmexPart, EmexPartsResult } from "../emex/emex.types";
/**
* Look up a vehicle's parts + schema for a category (PNC group) in the local
* EMEX dump (sase-catalog-src-emex MariaDB). Returns null on any miss so the
* caller falls through to the live emex scrape.
* Look up parts + schema for an EMEX category in the local dump
* (sase-catalog-src-emex MariaDB). Returns null on any miss so the caller
* falls through to the live emex scrape.
*
* Inputs:
* - vehicleSsd: emex's per-vehicle session-state-descriptor — captured into
* `vehicles.rawData.emexSsd` during live VIN decode. Required (no VIN
* column in the dump; SSD is the only stable vehicle identifier).
* - categoryUrl: the QuickDetails.aspx URL stored in `categories.linkPath`.
* We parse `gid` (the group id) out of it.
* Bridge: catalogs.code ↔ sase's vehicles.rawData.catalogCode,
* part_groups.group_id (varchar) ↔ sase's categories.externalId (gid).
*
* SAFETY (2026-06-01 audit): the catalog-wide bridge below (no per-vehicle
* filter) measured a 7-114x noiseRatio across every sampled catalog and
* 49-98 wrong-OEM per 100 parts served, because vehicle_parts is shared
* densely across all variants in a (catalog, group) pair (e.g. Renault
* Mégane vs Duster vs Clio share the same gids). That directly violates the
* "always correct OEM, no exceptions" rule. To prevent leakage, fetchCategoryParts
* now returns null unless the catalogCode is in the EMEX_SOURCE_DB_ALLOWED_CATALOGS
* allowlist — empty by default. A catalog should ONLY be added once a
* per-vehicle bridge (vehicles.unique_key reconstruction from raw_data.parsedOptions)
* lands and has been verified OEM-by-OEM vs live. All alternative bridges (SSD,
* api_cache, wizard_parameters, scrape_queue_v2.vehicle_ssd) were verified
* dead — see memory `sase-emex-source-db-safety.md` for the full inventory.
*
* The connection pool stays alive (master + emex switches default-on) so
* follow-up code can use it for per-vehicle queries / schema-only diagrams
* without having to flip env again.
*/
@Injectable()
export class EmexSourceDbService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(EmexSourceDbService.name);
private pool: Pool | null = null;
private enabled = false;
private allowedCatalogs: Set<string> = new Set();
constructor(private readonly config: ConfigService) {}
onModuleInit() {
const enabled = this.config.get<boolean>("catalogSource.enabled");
const masterEnabled = this.config.get<boolean>("catalogSource.enabled");
const emexEnabled = this.config.get<boolean>("catalogSource.emexEnabled");
const url = this.config.get<string>("catalogSource.emexUrl");
if (!enabled || !url) {
const allowed = this.config.get<string[]>("catalogSource.emexAllowedCatalogs") ?? [];
this.allowedCatalogs = new Set(allowed);
if (!masterEnabled || !emexEnabled || !url) {
this.logger.log(
`[emex-src] disabled (enabled=${enabled}, urlSet=${Boolean(url)}); upstream-only`,
`[emex-src] disabled (master=${masterEnabled}, emex=${emexEnabled}, urlSet=${Boolean(
url,
)}); upstream-only`,
);
return;
}
@@ -39,7 +58,15 @@ export class EmexSourceDbService implements OnModuleInit, OnModuleDestroy {
waitForConnections: true,
});
this.enabled = true;
this.logger.log("[emex-src] connected, lookup-first enabled");
if (this.allowedCatalogs.size === 0) {
this.logger.warn(
"[emex-src] connected but ALLOWLIST EMPTY — all fetchCategoryParts calls return null until EMEX_SOURCE_DB_ALLOWED_CATALOGS is populated (safety: catalog-wide bridge has 7-114x noise; see sase-emex-source-db-safety memory)",
);
} else {
this.logger.log(
`[emex-src] connected, allowlist: [${[...this.allowedCatalogs].sort().join(",")}]`,
);
}
}
async onModuleDestroy() {
@@ -50,45 +77,45 @@ export class EmexSourceDbService implements OnModuleInit, OnModuleDestroy {
}
/**
* Mirror the live `EmexCatalogService.fetchCategoryParts` shape.
* Mirror the live `EmexService.fetchCategoryParts` shape.
* `catalogCode` = vehicles.rawData.catalogCode (e.g. "RENAULT201910"),
* `gid` = categories.externalId (e.g. "11754").
* Returns null on miss; never throws.
*/
async fetchCategoryParts(
vehicleSsd: string | null | undefined,
categoryUrl: string,
catalogCode: string | null | undefined,
gid: string | null | undefined,
): Promise<EmexPartsResult | null> {
if (!this.enabled || !this.pool) return null;
if (!vehicleSsd) return null; // no SSD captured → can't locate vehicle in dump
const gid = extractGid(categoryUrl);
if (!gid) return null;
if (!catalogCode || !gid) return null;
// Safety gate: the only path currently implemented (catalog-wide DISTINCT
// on vehicle_parts.group_id, ignoring vehicle_id) is unsafe — see class
// doc. Until a per-vehicle bridge lands, allow no catalogs.
if (!this.allowedCatalogs.has(catalogCode)) return null;
try {
// 1) Resolve vehicle.id from SSD. We try `ssd = ?` first; if the dump
// canonicalized into `unique_key` (hash), the second arm catches it.
const [vehicleRows] = await this.pool.execute<RowDataPacket[]>(
"SELECT id, catalog_id FROM vehicles WHERE ssd = ? OR unique_key = ? LIMIT 1",
[vehicleSsd, vehicleSsd],
);
if (vehicleRows.length === 0) return null;
const vehicleId = vehicleRows[0].id as number;
const catalogId = vehicleRows[0].catalog_id as number;
// 2) Resolve part_group.id from external gid scoped to this catalog.
// Resolve (catalog.id, part_group.id) in one round trip.
const [groupRows] = await this.pool.execute<RowDataPacket[]>(
"SELECT id FROM part_groups WHERE catalog_id = ? AND group_id = ? LIMIT 1",
[catalogId, gid],
`SELECT pg.id AS pg_id, pg.catalog_id
FROM catalogs c
JOIN part_groups pg ON pg.catalog_id = c.id
WHERE c.code = ? AND pg.group_id = ?
LIMIT 1`,
[catalogCode, gid],
);
if (groupRows.length === 0) return null;
const groupPk = groupRows[0].id as number;
const groupPk = groupRows[0].pg_id as number;
// 3) Parts for this vehicle in this group.
// Parts: catalog-wide via vehicle_parts (no vehicle filter — see class
// comment). DISTINCT collapses the same part being linked from many
// vehicles in the catalog.
const [partRows] = await this.pool.execute<RowDataPacket[]>(
`SELECT p.id AS part_id, p.part_number, p.name, p.position_number, p.pnc
`SELECT DISTINCT p.id AS part_id, p.part_number, p.name, p.position_number, p.pnc
FROM vehicle_parts vp
JOIN parts p ON p.id = vp.part_id
WHERE vp.vehicle_id = ? AND vp.group_id = ?
WHERE vp.group_id = ?
ORDER BY p.position_number, p.id`,
[vehicleId, groupPk],
[groupPk],
);
const parts: EmexPart[] = partRows.map((r) => ({
@@ -97,11 +124,11 @@ export class EmexSourceDbService implements OnModuleInit, OnModuleDestroy {
positionCode: r.position_number ?? r.pnc ?? undefined,
}));
// 4) Schema image + hotspots for this group.
// Schema image + hotspots for this group.
const [imgRows] = await this.pool.execute<RowDataPacket[]>(
`SELECT original_url, width, height, hotspots
FROM part_images
WHERE group_id = ? AND image_type IN ('DIAGRAM','SCHEMATIC')
WHERE group_id = ? AND image_type IN ('DIAGRAM', 'SCHEMATIC')
ORDER BY is_primary DESC, sort_order
LIMIT 1`,
[groupPk],
@@ -135,24 +162,14 @@ export class EmexSourceDbService implements OnModuleInit, OnModuleDestroy {
return { parts, schemaImageUrl, hotspots, schemaWidth, schemaHeight };
} catch (err) {
this.logger.warn(`[emex-src] lookup failed (gid=${gid}): ${(err as Error).message}`);
this.logger.warn(
`[emex-src] lookup failed (${catalogCode}/${gid}): ${(err as Error).message}`,
);
return null;
}
}
}
/** Extract `gid=...` from a QuickDetails.aspx / similar URL. */
function extractGid(url: string): string | null {
if (!url) return null;
const m = url.match(/[?&]gid=([^&#]+)/i);
if (!m) return null;
try {
return decodeURIComponent(m[1]);
} catch {
return m[1];
}
}
/**
* Convert the dump's hotspots JSON ([{x,y,w,h,part_id}]) into the EmexHotspot
* shape used by the live scraper (grouped by position code).

View File

@@ -24,11 +24,19 @@ export class PcatSourceDbService implements OnModuleInit, OnModuleDestroy {
constructor(private readonly config: ConfigService) {}
onModuleInit() {
const enabled = this.config.get<boolean>("catalogSource.enabled");
const masterEnabled = this.config.get<boolean>("catalogSource.enabled");
const pcatEnabled = this.config.get<boolean>("catalogSource.pcatEnabled");
const url = this.config.get<string>("catalogSource.pcatUrl");
if (!enabled || !url) {
// Default off: verified 2026-06-01 that the current pcat dump's deep-scrape
// (7.978 cars with real parts) targets US/JDM-market models (Toyota 2112,
// Nissan 1508, Audi 1311, Chevy 1050, Hyundai 745) and covers 0 of sase's
// 103 dev TR-market pcat carIds via either bridge (schema_parts or
// part_groups+part_group_items). Container stays up for future use cases.
if (!masterEnabled || !pcatEnabled || !url) {
this.logger.log(
`[pcat-src] disabled (enabled=${enabled}, urlSet=${Boolean(url)}); upstream-only`,
`[pcat-src] disabled (master=${masterEnabled}, pcat=${pcatEnabled}, urlSet=${Boolean(
url,
)}); upstream-only`,
);
return;
}

View File

@@ -51,6 +51,9 @@ async function bootstrap() {
"https://challenges.cloudflare.com",
"https://destek.sase.tr",
"wss://destek.sase.tr",
// Sentry browser SDK envelope POSTs (otolog org, de region).
// Without this CSP silently blocks every error/replay upload.
"https://*.ingest.de.sentry.io",
],
objectSrc: ["'none'"],
frameSrc: ["https://challenges.cloudflare.com", "https://destek.sase.tr"],

View File

@@ -22,6 +22,7 @@
"@remotion/player": "^4.0.422",
"@sase/shared": "workspace:*",
"@sase/ui": "workspace:*",
"@sentry/react": "^9.0.0",
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-router": "^1.120.0",
"better-auth": "^1.2.0",

View File

@@ -0,0 +1,57 @@
/**
* Sentry browser SDK init — gated on VITE_SENTRY_DSN.
*
* Co-exists with Faro (Grafana) and PostHog: Sentry handles uncaught JS errors
* and on-error session replay; Faro handles RUM/traces; PostHog handles product
* analytics. No conflict between the three at the global error-handler layer.
*
* KVKK note: `sendDefaultPii: false` and `maskAllText: true` on replay.
*/
import type { init as SentryInit } from "@sentry/react";
let initialized = false;
export async function initSentry() {
if (initialized) return;
const dsn = import.meta.env.VITE_SENTRY_DSN;
if (!dsn) {
if (import.meta.env.DEV) {
console.info("[sentry] VITE_SENTRY_DSN unset — skipping init");
}
return;
}
const environment =
import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE ?? "production";
const release = import.meta.env.VITE_SENTRY_RELEASE;
try {
const Sentry = await import("@sentry/react");
(Sentry.init as typeof SentryInit)({
dsn,
environment,
release,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
],
// Performance: 10% trace sample rate (low volume site, can raise later).
tracesSampleRate: 0.1,
// Replay: only when an error fires; no session-replay otherwise (cost + privacy).
replaysSessionSampleRate: 0.0,
replaysOnErrorSampleRate: 1.0,
// KVKK: do not auto-attach IP / cookies / user agent fingerprints.
sendDefaultPii: false,
// Drop framework noise that isn't actionable.
ignoreErrors: [
"ResizeObserver loop limit exceeded",
"ResizeObserver loop completed with undelivered notifications.",
"Non-Error promise rejection captured",
],
});
initialized = true;
console.log("[sentry] browser SDK initialized");
} catch (err) {
console.warn("[sentry] init failed:", (err as Error).message);
}
}

View File

@@ -7,9 +7,13 @@ import { initFaro } from "./lib/faro";
import { initLocale } from "./lib/i18n";
import { initMetaPixel } from "./lib/meta-pixel";
import { initPostHog } from "./lib/posthog";
import { initSentry } from "./lib/sentry";
import { routeTree } from "./routeTree.gen";
import "./globals.css";
// Sentry first so it can capture errors thrown by other init code.
initSentry();
// Read persisted locale from localStorage before first render — without this
// a reload always falls back to the TR default even if the user picked EN.
initLocale();

View File

@@ -30,6 +30,9 @@ export default defineConfig({
},
build: {
outDir: "dist",
sourcemap: false,
// 'hidden' emits source maps but strips the //# sourceMappingURL comment
// from JS bundles. Sentry can still consume them (via release upload), but
// browsers won't fetch them, so end-user stack traces stay minified.
sourcemap: "hidden",
},
});

View File

@@ -10,6 +10,11 @@ services:
- VITE_META_PIXEL_ID=${VITE_META_PIXEL_ID:-}
- VITE_CHATWOOT_BASE_URL=${VITE_CHATWOOT_BASE_URL:-}
- VITE_CHATWOOT_WEBSITE_TOKEN=${VITE_CHATWOOT_WEBSITE_TOKEN:-}
# Sentry browser SDK — gated on DSN presence (unset = skip init).
# VITE_* args must be build-time; runtime env won't reach the bundle.
- VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-}
- VITE_SENTRY_ENVIRONMENT=${VITE_SENTRY_ENVIRONMENT:-production}
- VITE_SENTRY_RELEASE=${VITE_SENTRY_RELEASE:-}
environment:
- NODE_ENV=production
- PORT=4000
@@ -68,6 +73,20 @@ services:
- ML_PREDICTION_ENABLED=${ML_PREDICTION_ENABLED:-false}
# Chatwoot live-chat widget — HMAC secret for verified user identity
- CHATWOOT_HMAC_TOKEN=${CHATWOOT_HMAC_TOKEN:-}
# Local catalog-dump lookup (CategoriesService → PcatSourceDb / EmexSourceDb).
# Defaults off; when CATALOG_SOURCE_DB_ENABLED=true + URLs set, parts fetches
# hit the dump first and only fall through to live upstream on miss.
- CATALOG_SOURCE_DB_ENABLED=${CATALOG_SOURCE_DB_ENABLED:-false}
- PCAT_SOURCE_DB_URL=${PCAT_SOURCE_DB_URL:-}
- EMEX_SOURCE_DB_URL=${EMEX_SOURCE_DB_URL:-}
# Per-source kill switches. pcat default off (dump doesn't cover TR vehicles).
- EMEX_SOURCE_DB_ENABLED=${EMEX_SOURCE_DB_ENABLED:-true}
- PCAT_SOURCE_DB_ENABLED=${PCAT_SOURCE_DB_ENABLED:-false}
# Per-catalog parts-lookup allowlist (comma-separated). Empty → all
# fetchCategoryParts calls return null. See sase-emex-source-db-safety
# memory: catalog-wide bridge had 7-114x noise; only add a catalog once
# its per-vehicle bridge is wired & OEM-verified.
- EMEX_SOURCE_DB_ALLOWED_CATALOGS=${EMEX_SOURCE_DB_ALLOWED_CATALOGS:-}
depends_on:
sase-redis:
condition: service_healthy
@@ -130,6 +149,18 @@ services:
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-sase-worker}
- OTEL_TRACE_SAMPLE_RATE=${OTEL_TRACE_SAMPLE_RATE:-1.0}
# Local catalog-dump lookup (worker drives prefetch which uses CategoriesService).
- CATALOG_SOURCE_DB_ENABLED=${CATALOG_SOURCE_DB_ENABLED:-false}
- PCAT_SOURCE_DB_URL=${PCAT_SOURCE_DB_URL:-}
- EMEX_SOURCE_DB_URL=${EMEX_SOURCE_DB_URL:-}
# Per-source kill switches. pcat default off (dump doesn't cover TR vehicles).
- EMEX_SOURCE_DB_ENABLED=${EMEX_SOURCE_DB_ENABLED:-true}
- PCAT_SOURCE_DB_ENABLED=${PCAT_SOURCE_DB_ENABLED:-false}
# Per-catalog parts-lookup allowlist (comma-separated). Empty → all
# fetchCategoryParts calls return null. See sase-emex-source-db-safety
# memory: catalog-wide bridge had 7-114x noise; only add a catalog once
# its per-vehicle bridge is wired & OEM-verified.
- EMEX_SOURCE_DB_ALLOWED_CATALOGS=${EMEX_SOURCE_DB_ALLOWED_CATALOGS:-}
depends_on:
sase-redis:
condition: service_healthy

View File

@@ -120,6 +120,35 @@ export const envSchema = z.object({
.default("false"),
PCAT_SOURCE_DB_URL: z.string().url().optional(),
EMEX_SOURCE_DB_URL: z.string().optional(), // mysql://... — not a strict URL per WHATWG
// Per-source kill switches under the master CATALOG_SOURCE_DB_ENABLED.
// EMEX_SOURCE_DB_ENABLED keeps the connection pool alive but, per the
// 2026-06-01 safety audit, fetchCategoryParts ALWAYS returns null unless the
// requested catalogCode is also in EMEX_SOURCE_DB_ALLOWED_CATALOGS. The
// catalog-wide bridge measured 7-114x noiseRatio across every catalog and
// 49-98 wrong-OEM per 100 served — direct violation of the "always correct
// OEM" rule. Default allowlist is EMPTY → behaviour is safe by default; the
// master/emex switches stay default-on so the service is ready for the
// per-vehicle unique_key bridge (follow-up work).
// PCAT_SOURCE_DB_ENABLED defaults FALSE — verified 2026-06-01 that the dump's
// deep-scrape covers a US/JDM market subset (Toyota/Nissan/Audi/Chevy/Hyundai)
// that doesn't intersect sase's TR-market vehicle pool (0 / 103 dev carIds
// had real parts data through either bridge). Container stays running for
// future use cases (OEM cross-ref, alt-part search).
EMEX_SOURCE_DB_ENABLED: z
.string()
.transform((v) => v === "true")
.default("true"),
PCAT_SOURCE_DB_ENABLED: z
.string()
.transform((v) => v === "true")
.default("false"),
// Per-catalog parts-lookup allowlist. Comma-separated catalog codes
// (e.g. "RENAULT201910,FFIAT84"). Empty (default) → fetchCategoryParts
// always returns null → live emex handles every request. A catalog SHOULD
// only be added here AFTER its per-vehicle bridge (unique_key) is wired and
// verified against live OEM-by-OEM on at least 5 sampled vehicles. See
// memory `sase-emex-source-db-safety.md` for the bridge inventory & audit.
EMEX_SOURCE_DB_ALLOWED_CATALOGS: z.string().default(""),
});
export type Env = z.infer<typeof envSchema>;

80
pnpm-lock.yaml generated
View File

@@ -214,6 +214,9 @@ importers:
'@sase/ui':
specifier: workspace:*
version: link:../../packages/ui
'@sentry/react':
specifier: ^9.0.0
version: 9.47.1(react@19.2.4)
'@tanstack/react-query':
specifier: ^5.62.0
version: 5.90.21(react@19.2.4)
@@ -2864,14 +2867,38 @@ packages:
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@sentry-internal/browser-utils@9.47.1':
resolution: {integrity: sha512-twv6YhrUlPkvKz4/iQDH4KHgcv9t4cMjmZPf4/dCSCXn4/GOjzjx2d74c1w+1KOdS7lcsQzI+MtbK6SeYLiGfQ==}
engines: {node: '>=18'}
'@sentry-internal/feedback@9.47.1':
resolution: {integrity: sha512-xJ4vKvIpAT8e+Sz80YrsNinPU0XV7jPxPjdZ4ex8R2mMvx7pM0gq8JiR/sIVmNiOE0WiUDr6VwLDE8j2APSRMA==}
engines: {node: '>=18'}
'@sentry-internal/node-cpu-profiler@2.4.0':
resolution: {integrity: sha512-zMrbqkd05LS1Ibt+js4R1aMmjdAO0yi9xiywWeulYs/bxN8P5qq20QHYleI76MorsocvYJAFo9GkYfzyzMd6Og==}
engines: {node: '>=18'}
'@sentry-internal/replay-canvas@9.47.1':
resolution: {integrity: sha512-r9nve+l5+elGB9NXSN1+PUgJy790tXN1e8lZNH2ziveoU91jW4yYYt34mHZ30fU9tOz58OpaRMj3H3GJ/jYZVA==}
engines: {node: '>=18'}
'@sentry-internal/replay@9.47.1':
resolution: {integrity: sha512-O9ZEfySpstGtX1f73m3NbdbS2utwPikaFt6sgp74RG4ZX4LlXe99VAjKR464xKECpYsLmj2bYpiK4opURF0pBA==}
engines: {node: '>=18'}
'@sentry/browser@9.47.1':
resolution: {integrity: sha512-at5JOLziw5QpVYytxTDU6xijdV6lDQ/Rxp/qXJaHXud3gIK4suv2cXW+tupJfwoUoHFCnDNfccjCmPmP0yRqiA==}
engines: {node: '>=18'}
'@sentry/core@10.52.0':
resolution: {integrity: sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A==}
engines: {node: '>=18'}
'@sentry/core@9.47.1':
resolution: {integrity: sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==}
engines: {node: '>=18'}
'@sentry/nestjs@10.52.0':
resolution: {integrity: sha512-biGgCtQ6kBg5dthoaZMdg4frcLk1To1rfP4TB7njBmRhR0nRfvNBsyqc33afkhxCu2UOuJ+ucOKgh3+LUSOv8Q==}
engines: {node: '>=18'}
@@ -2921,6 +2948,12 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@sentry/react@9.47.1':
resolution: {integrity: sha512-Anqt0hG1R+nktlwEiDc2FmD+6DUGMJOLuArgr7q1cSCdPbK2Gb1eZ2rF57Ui+CDo9XLvlX9QP2is/M08rrVe3w==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
'@smithy/abort-controller@4.2.8':
resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==}
engines: {node: '>=18.0.0'}
@@ -4589,6 +4622,9 @@ packages:
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
engines: {node: '>=18.0.0'}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -5501,6 +5537,9 @@ packages:
peerDependencies:
react: ^19.2.4
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -8992,13 +9031,41 @@ snapshots:
'@scarf/scarf@1.4.0': {}
'@sentry-internal/browser-utils@9.47.1':
dependencies:
'@sentry/core': 9.47.1
'@sentry-internal/feedback@9.47.1':
dependencies:
'@sentry/core': 9.47.1
'@sentry-internal/node-cpu-profiler@2.4.0':
dependencies:
detect-libc: 2.1.2
node-abi: 3.92.0
'@sentry-internal/replay-canvas@9.47.1':
dependencies:
'@sentry-internal/replay': 9.47.1
'@sentry/core': 9.47.1
'@sentry-internal/replay@9.47.1':
dependencies:
'@sentry-internal/browser-utils': 9.47.1
'@sentry/core': 9.47.1
'@sentry/browser@9.47.1':
dependencies:
'@sentry-internal/browser-utils': 9.47.1
'@sentry-internal/feedback': 9.47.1
'@sentry-internal/replay': 9.47.1
'@sentry-internal/replay-canvas': 9.47.1
'@sentry/core': 9.47.1
'@sentry/core@10.52.0': {}
'@sentry/core@9.47.1': {}
'@sentry/nestjs@10.52.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@opentelemetry/exporter-trace-otlp-http@0.212.0(@opentelemetry/api@1.9.0))':
dependencies:
'@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -9079,6 +9146,13 @@ snapshots:
- '@opentelemetry/exporter-trace-otlp-http'
- supports-color
'@sentry/react@9.47.1(react@19.2.4)':
dependencies:
'@sentry/browser': 9.47.1
'@sentry/core': 9.47.1
hoist-non-react-statics: 3.3.2
react: 19.2.4
'@smithy/abort-controller@4.2.8':
dependencies:
'@smithy/types': 4.12.0
@@ -10981,6 +11055,10 @@ snapshots:
helmet@8.1.0: {}
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
dependencies:
'@exodus/bytes': 1.13.0(@noble/hashes@2.0.1)
@@ -12124,6 +12202,8 @@ snapshots:
react: 19.2.4
scheduler: 0.27.0
react-is@16.13.1: {}
react-is@17.0.2: {}
react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4):