feat: add marketplace overview audit and injection success metrics endpoint
This commit is contained in:
parent
7de04ab4e0
commit
156f7cdd58
54
docs/developer-portal/00-marketplace-overview-audit.md
Normal file
54
docs/developer-portal/00-marketplace-overview-audit.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# 00 Marketplace Overview Audit
|
||||||
|
|
||||||
|
Source checked: Uber Eats Marketplace APIs Overview section shared by you.
|
||||||
|
|
||||||
|
## Status Matrix (Doc vs Current Code)
|
||||||
|
|
||||||
|
- Integration Configuration:
|
||||||
|
Implemented partly. Merchant and OAuth connection setup exists.
|
||||||
|
Files: `src/modules/connections`, `src/modules/auth`.
|
||||||
|
Pending: typed onboarding endpoints from official Integration Configuration docs.
|
||||||
|
|
||||||
|
- Menu:
|
||||||
|
Implemented partly.
|
||||||
|
Implemented: menu upsert/get wrappers.
|
||||||
|
Files: `src/modules/proxy/proxy.service.js`, `src/routes/proxy.routes.js`.
|
||||||
|
Pending: full endpoint set from official Menu docs.
|
||||||
|
|
||||||
|
- Store:
|
||||||
|
Implemented partly.
|
||||||
|
Implemented: update store hours shortcut.
|
||||||
|
Pending: full store details/status/holiday hours typed routes.
|
||||||
|
|
||||||
|
- Order:
|
||||||
|
Implemented partly.
|
||||||
|
Implemented: list orders and order actions (accept/deny/ready/cancel).
|
||||||
|
Pending: all order lifecycle endpoints from official Order docs, including detailed states and BYOC-specific flows.
|
||||||
|
|
||||||
|
- Promotions:
|
||||||
|
Not implemented as typed APIs yet.
|
||||||
|
Temporary coverage: generic passthrough endpoint.
|
||||||
|
|
||||||
|
- Reporting:
|
||||||
|
Not implemented as typed APIs yet.
|
||||||
|
Temporary coverage: generic passthrough endpoint.
|
||||||
|
|
||||||
|
- Webhooks:
|
||||||
|
Implemented basic ingestion and persistence.
|
||||||
|
Pending: signature verification, idempotency key enforcement, async processing pipeline.
|
||||||
|
|
||||||
|
## Production Requirement Alignment
|
||||||
|
|
||||||
|
- 99% injection success tracking:
|
||||||
|
Implemented initial metric endpoint:
|
||||||
|
`GET /api/v1/metrics/injection-success`
|
||||||
|
based on logged responses for `accept_pos_order`.
|
||||||
|
|
||||||
|
## Important Current Constraint
|
||||||
|
|
||||||
|
Until endpoint-specific docs are provided, the wrapper supports full coverage through:
|
||||||
|
|
||||||
|
- `POST /api/v1/uber/request`
|
||||||
|
|
||||||
|
Typed endpoint expansion will continue section-by-section as you send official docs.
|
||||||
|
|
||||||
@ -6,4 +6,4 @@ Go-live readiness:
|
|||||||
- Webhook URL reachable over HTTPS
|
- Webhook URL reachable over HTTPS
|
||||||
- Alerting and log monitoring enabled
|
- Alerting and log monitoring enabled
|
||||||
- Token refresh and failure runbooks ready
|
- Token refresh and failure runbooks ready
|
||||||
|
- Injection success rate monitoring enabled (`target >= 99%`)
|
||||||
|
|||||||
@ -145,6 +145,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/metrics/injection-success": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Get order injection success metric",
|
||||||
|
"tags": [
|
||||||
|
"Metrics"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "merchantId",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "windowDays",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Injection metrics"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/uber/request": {
|
"/api/v1/uber/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Generic Uber passthrough for any Uber endpoint",
|
"summary": "Generic Uber passthrough for any Uber endpoint",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const authRoutes = require("./routes/auth.routes");
|
|||||||
const connectionRoutes = require("./routes/connections.routes");
|
const connectionRoutes = require("./routes/connections.routes");
|
||||||
const proxyRoutes = require("./routes/proxy.routes");
|
const proxyRoutes = require("./routes/proxy.routes");
|
||||||
const webhookRoutes = require("./routes/webhooks.routes");
|
const webhookRoutes = require("./routes/webhooks.routes");
|
||||||
|
const metricsRoutes = require("./routes/metrics.routes");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -25,10 +26,10 @@ app.use("/health", healthRoutes);
|
|||||||
app.use("/api/v1/auth", requireWrapperApiKey, authRoutes);
|
app.use("/api/v1/auth", requireWrapperApiKey, authRoutes);
|
||||||
app.use("/api/v1", requireWrapperApiKey, connectionRoutes);
|
app.use("/api/v1", requireWrapperApiKey, connectionRoutes);
|
||||||
app.use("/api/v1", requireWrapperApiKey, proxyRoutes);
|
app.use("/api/v1", requireWrapperApiKey, proxyRoutes);
|
||||||
|
app.use("/api/v1", requireWrapperApiKey, metricsRoutes);
|
||||||
app.use("/api/v1", webhookRoutes);
|
app.use("/api/v1", webhookRoutes);
|
||||||
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
|
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|
||||||
|
|||||||
@ -148,6 +148,46 @@ const apiLogRepository = {
|
|||||||
`);
|
`);
|
||||||
stmt.run(row);
|
stmt.run(row);
|
||||||
return row;
|
return row;
|
||||||
|
},
|
||||||
|
|
||||||
|
getInjectionSuccessStats({ merchantId, sinceIso }) {
|
||||||
|
const params = [];
|
||||||
|
const filters = ["uber_path LIKE '%/accept_pos_order%'"];
|
||||||
|
|
||||||
|
if (merchantId) {
|
||||||
|
filters.push("merchant_id = ?");
|
||||||
|
params.push(merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sinceIso) {
|
||||||
|
filters.push("created_at >= ?");
|
||||||
|
params.push(sinceIso);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereSql = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
||||||
|
|
||||||
|
const totals = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total,
|
||||||
|
SUM(CASE WHEN response_status >= 200 AND response_status < 300 THEN 1 ELSE 0 END) AS success
|
||||||
|
FROM api_logs
|
||||||
|
${whereSql}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(...params);
|
||||||
|
|
||||||
|
const total = totals?.total || 0;
|
||||||
|
const success = totals?.success || 0;
|
||||||
|
const successRate = total === 0 ? 0 : Number(((success / total) * 100).toFixed(2));
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
success,
|
||||||
|
failed: total - success,
|
||||||
|
successRate
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,4 +197,3 @@ module.exports = {
|
|||||||
webhookRepository,
|
webhookRepository,
|
||||||
apiLogRepository
|
apiLogRepository
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
44
src/modules/metrics/metrics.controller.js
Normal file
44
src/modules/metrics/metrics.controller.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const { z } = require("zod");
|
||||||
|
const { apiLogRepository } = require("../../db/adapter");
|
||||||
|
|
||||||
|
function toSinceIso(windowDays) {
|
||||||
|
if (!windowDays) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const days = Number(windowDays);
|
||||||
|
if (Number.isNaN(days) || days <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ms = days * 24 * 60 * 60 * 1000;
|
||||||
|
return new Date(Date.now() - ms).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInjectionSuccess(req, res) {
|
||||||
|
const schema = z.object({
|
||||||
|
merchantId: z.string().optional(),
|
||||||
|
windowDays: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = schema.parse(req.query);
|
||||||
|
const sinceIso = toSinceIso(query.windowDays);
|
||||||
|
const stats = apiLogRepository.getInjectionSuccessStats({
|
||||||
|
merchantId: query.merchantId,
|
||||||
|
sinceIso
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
metric: "order_injection_success_rate",
|
||||||
|
merchantId: query.merchantId || "all",
|
||||||
|
windowDays: query.windowDays ? Number(query.windowDays) : "all_time",
|
||||||
|
targetPercent: 99,
|
||||||
|
...stats
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getInjectionSuccess
|
||||||
|
};
|
||||||
|
|
||||||
32
src/routes/metrics.routes.js
Normal file
32
src/routes/metrics.routes.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const asyncHandler = require("../middleware/asyncHandler");
|
||||||
|
const { getInjectionSuccess } = require("../modules/metrics/metrics.controller");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /api/v1/metrics/injection-success:
|
||||||
|
* get:
|
||||||
|
* summary: Get order injection success metric
|
||||||
|
* tags:
|
||||||
|
* - Metrics
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: merchantId
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: windowDays
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Injection metrics
|
||||||
|
*/
|
||||||
|
router.get("/metrics/injection-success", asyncHandler(getInjectionSuccess));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user