feat(social): emit GrowQR events from social-branding proxy routes
The social-branding proxy was a raw passthrough that never called
recordGatewayEvent — so no grow_events rows were created for any social
action (account connect, profile polish, post publish, etc.), breaking
the curator/streak/QScore loop.
Follow the proxyResumeRequest pattern exactly:
- GET/HEAD pass through directly (no event)
- POST/PUT/PATCH/DELETE buffer response, record gateway event, return
- Add socialEventTypeForRest() mapping 10+ social routes to event types
- Add 'social' to canonicalSubjectServiceId mapping
- Event payload is strictly {path, method, status, action} — no raw
cookies, tokens, passwords, or LinkedIn session data
- Extract account_id from path/response for correlation.externalId
This commit is contained in:
+67
-3
@@ -85,6 +85,7 @@ function canonicalSubjectServiceId(source: string) {
|
||||
if (source.includes("interview")) return "interview";
|
||||
if (source.includes("roleplay")) return "roleplay";
|
||||
if (source.includes("resume")) return "resume";
|
||||
if (source.includes("social")) return "social";
|
||||
if (source.includes("qscore")) return "qscore";
|
||||
if (source.includes("matchmaking")) return "matchmaking";
|
||||
if (source.includes("analytics")) return "analytics";
|
||||
@@ -218,6 +219,23 @@ function resumeEventTypeForRest(method: string, rest: string, ok: boolean) {
|
||||
return "resume.loaded";
|
||||
}
|
||||
|
||||
function socialEventTypeForRest(method: string, rest: string, ok: boolean) {
|
||||
if (!ok) return "social.request_failed";
|
||||
const m = method.toUpperCase();
|
||||
const path = rest.replace(/^\/+/, "").split("?")[0] ?? rest;
|
||||
if (m === "POST" && /^accounts\/connect/.test(path)) return "social.account.connected";
|
||||
if (m === "POST" && /^accounts\/[^/]+\/profile\/polish/.test(path)) return "social.profile.polished";
|
||||
if (m === "POST" && /^accounts\/[^/]+\/profile\/refresh/.test(path)) return "social.profile.refreshed";
|
||||
if (m === "POST" && /^accounts\/[^/]+\/profile\/apply/.test(path)) return "social.profile.updated";
|
||||
if (m === "POST" && /^accounts\/[^/]+\/profile\/refine-experience/.test(path)) return "social.experience.refined";
|
||||
if (m === "POST" && /^profiles\/fetch-by-url/.test(path)) return "social.profile.fetched";
|
||||
if (m === "POST" && /^accounts\/[^/]+\/(content|posts)/.test(path)) return "social.content.generated";
|
||||
if (m !== "GET" && m !== "HEAD" && /^accounts\/[^/]+\/schedule/.test(path)) return "social.post.scheduled";
|
||||
if (m !== "GET" && m !== "HEAD" && /^accounts\/[^/]+\/publish/.test(path)) return "social.post.published";
|
||||
if (m === "POST" || m === "PUT" || m === "PATCH") return "social.updated";
|
||||
return "social.updated";
|
||||
}
|
||||
|
||||
function agentDataAction(result: Record<string, unknown>) {
|
||||
const messages = Array.isArray(result.messages) ? result.messages : [];
|
||||
for (const item of messages) {
|
||||
@@ -581,8 +599,13 @@ async function buildPersonalizedRoleplayConfigurePayload(req: Request, body: Jso
|
||||
async function proxySocialRequest(req: Request, rest: string, userId: string) {
|
||||
const incoming = new URL(req.url);
|
||||
const normalizedRest = rest.replace(/^\/+/, "");
|
||||
const forwardedQuery = new URLSearchParams(incoming.searchParams);
|
||||
forwardedQuery.delete("missionInstanceId");
|
||||
forwardedQuery.delete("missionId");
|
||||
forwardedQuery.delete("stageId");
|
||||
forwardedQuery.delete("source");
|
||||
const target = new URL(
|
||||
`/api/v1/${normalizedRest}${incoming.search}`,
|
||||
`/api/v1/${normalizedRest}${forwardedQuery.toString() ? `?${forwardedQuery.toString()}` : ""}`,
|
||||
config.socialBrandingServiceUrl.replace(/\/$/, ""),
|
||||
);
|
||||
|
||||
@@ -593,9 +616,50 @@ async function proxySocialRequest(req: Request, rest: string, userId: string) {
|
||||
|
||||
const method = req.method.toUpperCase();
|
||||
const body = ["GET", "HEAD"].includes(method) ? undefined : await req.arrayBuffer();
|
||||
const res = await fetch(target, { method, headers, body });
|
||||
const requestJson = parseJsonBody(body, headers);
|
||||
const mission = missionFromRequest(req, requestJson);
|
||||
// Strip mission fields from the forwarded body, mirroring the resume proxy,
|
||||
// so orchestration metadata is not passed downstream.
|
||||
const forwardBody =
|
||||
body && headers.get("content-type")?.includes("application/json")
|
||||
? Buffer.from(JSON.stringify(stripMissionFromBody(requestJson)))
|
||||
: body;
|
||||
if (forwardBody !== body) headers.delete("content-length");
|
||||
const res = await fetch(target, { method, headers, body: forwardBody });
|
||||
|
||||
return new Response(res.body, {
|
||||
if (method === "GET" || method === "HEAD") {
|
||||
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
||||
}
|
||||
|
||||
// Security note: the social-branding service handles LinkedIn sessions, tokens,
|
||||
// cookies, and credentials. The gateway event payload intentionally carries ONLY
|
||||
// canonical metadata (path/method/status/action) — never raw request/response bodies.
|
||||
const responseBuffer = await res.arrayBuffer();
|
||||
const responseText = Buffer.from(responseBuffer).toString("utf8");
|
||||
let responseJson: unknown;
|
||||
try { responseJson = responseText ? JSON.parse(responseText) : undefined; } catch { responseJson = undefined; }
|
||||
const responseObj = isRecord(responseJson) ? responseJson : {};
|
||||
|
||||
const eventType = socialEventTypeForRest(method, normalizedRest, res.ok);
|
||||
const derivedAction = eventType.split(".").slice(1).join(".");
|
||||
const accountMatch = normalizedRest.match(/^accounts\/(?!connect\/?$)[^/]+\/.+/);
|
||||
const externalId =
|
||||
(accountMatch ? accountMatch[0].split("/")[1] : undefined) ??
|
||||
getString(responseObj.account_id ?? responseObj.accountId ?? responseObj.id);
|
||||
|
||||
await recordGatewayEvent({
|
||||
userId,
|
||||
source: "agent-social-branding",
|
||||
type: eventType,
|
||||
payload: { path: normalizedRest, method, status: res.status, action: derivedAction },
|
||||
correlation: {
|
||||
externalId: externalId ?? `${canonicalSubjectServiceId("agent-social-branding")}:${eventType}`,
|
||||
taskId: curatorTaskIdFromRequest(req, requestJson),
|
||||
},
|
||||
mission,
|
||||
}).catch((err) => log.warn({ err, path: normalizedRest }, "failed to record social gateway event"));
|
||||
|
||||
return new Response(responseBuffer, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: res.headers,
|
||||
|
||||
Reference in New Issue
Block a user