- Add queryTimed() wrapper and logPerf() for per-endpoint timing metrics - Add api_perf_log table flush mechanism with background thread batching - Add application-scope cache (appCacheGet/Put/Invalidate) with TTL - Cache businesses/get (5m), addresses/states (24h), menu/items (2m) - Fix N+1 queries in orders/history, orders/listForKDS (batch fetch) - Fix correlated subquery in orders/getDetail (LEFT JOIN) - Combine 4 queries into 1 in portal/stats (subselects) - Optimize getForBuilder tree building with pre-indexed parent lookup - Add cache invalidation in update, saveBrandColor, updateHours, saveFromBuilder - New admin/perf.cfm dashboard (localhost-protected) - Instrument top 10 endpoints with queryTimed + logPerf Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
260 lines
8.6 KiB
Text
260 lines
8.6 KiB
Text
<cfscript>
|
|
/**
|
|
* Environment Configuration
|
|
*
|
|
* Controls dev vs production behavior.
|
|
* This file should have DIFFERENT values on dev vs biz servers.
|
|
*
|
|
* dev.payfrit.com: isDevEnvironment = true
|
|
* biz.payfrit.com: isDevEnvironment = false
|
|
*/
|
|
|
|
// ============================================
|
|
// ENVIRONMENT FLAG - CHANGE PER SERVER
|
|
// ============================================
|
|
isDevEnvironment = true; // Set to FALSE on biz.payfrit.com
|
|
|
|
// ============================================
|
|
// ERROR HANDLING
|
|
// ============================================
|
|
// Dev: Show full stack traces
|
|
// Prod: Show generic "server error" message
|
|
application.showDetailedErrors = isDevEnvironment;
|
|
|
|
// ============================================
|
|
// DEBUG LOGGING
|
|
// ============================================
|
|
// Dev: Log all API requests/responses
|
|
// Prod: Minimal logging
|
|
application.debugLogging = isDevEnvironment;
|
|
application.logDirectory = expandPath("/api/logs/");
|
|
|
|
// ============================================
|
|
// RATE LIMITING
|
|
// ============================================
|
|
// Dev: No rate limits
|
|
// Prod: Enforce rate limits
|
|
application.enableRateLimiting = !isDevEnvironment;
|
|
application.rateLimitPerMinute = 60; // requests per minute per IP
|
|
|
|
// ============================================
|
|
// EMAIL HANDLING
|
|
// ============================================
|
|
// Dev: Send all emails to catch-all address
|
|
// Prod: Send to real recipients
|
|
application.emailCatchAll = isDevEnvironment ? "dev-emails@payfrit.com" : "";
|
|
application.emailEnabled = !isDevEnvironment ? true : false; // Disable emails on dev entirely
|
|
|
|
// ============================================
|
|
// MAGIC OTP (for testing)
|
|
// ============================================
|
|
// Dev: Allow magic phone number to bypass OTP
|
|
// Prod: Disabled
|
|
application.MAGIC_OTP_ENABLED = isDevEnvironment;
|
|
application.MAGIC_OTP_CODE = "123456";
|
|
application.MAGIC_PHONE_NUMBERS = ["5555555555", "0000000000"];
|
|
|
|
// ============================================
|
|
// STRIPE MODE
|
|
// ============================================
|
|
// Already handled in stripe.cfm, but good to have reference
|
|
application.stripeTestMode = isDevEnvironment;
|
|
|
|
// ============================================
|
|
// API RESPONSE EXTRAS
|
|
// ============================================
|
|
// Dev: Include debug info in API responses (timing, queries, etc)
|
|
// Prod: Minimal responses
|
|
application.includeDebugInResponse = isDevEnvironment;
|
|
|
|
// ============================================
|
|
// SESSION/TOKEN SETTINGS
|
|
// ============================================
|
|
// Dev: Longer token expiry for easier testing
|
|
// Prod: Normal expiry
|
|
application.tokenExpiryHours = isDevEnvironment ? 720 : 24; // 30 days vs 1 day
|
|
|
|
// ============================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================
|
|
function isDev() {
|
|
return structKeyExists(application, "isDevEnvironment") && application.isDevEnvironment;
|
|
}
|
|
|
|
function logDebug(message, data = {}) {
|
|
if (!application.debugLogging) return;
|
|
|
|
var logFile = application.logDirectory & "debug_" & dateFormat(now(), "yyyy-mm-dd") & ".log";
|
|
var logLine = "[" & timeFormat(now(), "HH:mm:ss") & "] " & message;
|
|
|
|
if (!structIsEmpty(data)) {
|
|
logLine &= " | " & serializeJSON(data);
|
|
}
|
|
|
|
try {
|
|
if (!directoryExists(application.logDirectory)) {
|
|
directoryCreate(application.logDirectory);
|
|
}
|
|
fileAppend(logFile, logLine & chr(10));
|
|
} catch (any e) {
|
|
// Silent fail - don't break app if logging fails
|
|
}
|
|
}
|
|
|
|
function apiError(message, detail = "", statusCode = 500) {
|
|
var response = {
|
|
"OK": false,
|
|
"ERROR": "server_error",
|
|
"MESSAGE": application.showDetailedErrors ? message : "An error occurred"
|
|
};
|
|
|
|
if (application.showDetailedErrors && len(detail)) {
|
|
response["DETAIL"] = detail;
|
|
response["STACK"] = "";
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// ============================================
|
|
// PERFORMANCE PROFILING
|
|
// ============================================
|
|
application.perfEnabled = true; // Toggle profiling on/off
|
|
|
|
// Initialize perf buffer (once per application start)
|
|
if (!structKeyExists(application, "perfBuffer")) {
|
|
application.perfBuffer = [];
|
|
}
|
|
|
|
/**
|
|
* Drop-in replacement for queryExecute that tracks query count and time.
|
|
* Usage: queryTimed("SELECT ...", [...], { datasource: "payfrit" })
|
|
*/
|
|
function queryTimed(required string sql, any params = [], struct options = {}) {
|
|
var t = getTickCount();
|
|
var result = queryExecute(arguments.sql, arguments.params, arguments.options);
|
|
var elapsed = getTickCount() - t;
|
|
request._perf_queryCount++;
|
|
request._perf_queryTimeMs += elapsed;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Flush the in-memory perf buffer to the api_perf_log MySQL table.
|
|
* Called automatically when buffer reaches 100 entries.
|
|
*/
|
|
function flushPerfBuffer() {
|
|
var batch = [];
|
|
lock name="payfrit_perfBuffer" timeout="2" type="exclusive" {
|
|
batch = duplicate(application.perfBuffer);
|
|
application.perfBuffer = [];
|
|
}
|
|
if (arrayLen(batch) == 0) return;
|
|
|
|
try {
|
|
var sql = "INSERT INTO api_perf_log (Endpoint, TotalMs, DbMs, AppMs, QueryCount, ResponseBytes, BusinessID, UserID, LoggedAt) VALUES ";
|
|
var rows = [];
|
|
for (var m in batch) {
|
|
arrayAppend(rows,
|
|
"('" & replace(m.endpoint, "'", "''", "all") & "',"
|
|
& m.totalMs & "," & m.dbMs & "," & m.appMs & ","
|
|
& m.queryCount & "," & m.responseBytes & ","
|
|
& m.businessId & "," & m.userId & ",'"
|
|
& m.loggedAt & "')"
|
|
);
|
|
}
|
|
sql &= arrayToList(rows, ",");
|
|
queryExecute(sql, {}, { datasource: "payfrit" });
|
|
|
|
// Cleanup: delete rows older than 30 days (run occasionally)
|
|
if (randRange(1, 100) == 1) {
|
|
queryExecute(
|
|
"DELETE FROM api_perf_log WHERE LoggedAt < DATE_SUB(NOW(), INTERVAL 30 DAY)",
|
|
{}, { datasource: "payfrit" }
|
|
);
|
|
}
|
|
} catch (any e) {
|
|
// Silent fail - never break the app for profiling
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log performance metrics for the current request.
|
|
* Call this at the end of any instrumented endpoint, before the final writeOutput/abort.
|
|
*/
|
|
function logPerf(numeric responseBytes = 0) {
|
|
if (!structKeyExists(application, "perfEnabled") || !application.perfEnabled) return;
|
|
|
|
var totalMs = getTickCount() - request._perf_start;
|
|
var dbMs = request._perf_queryTimeMs;
|
|
|
|
var metric = {
|
|
endpoint: structKeyExists(request, "_api_path") ? request._api_path : "unknown",
|
|
totalMs: totalMs,
|
|
dbMs: dbMs,
|
|
appMs: totalMs - dbMs,
|
|
queryCount: request._perf_queryCount,
|
|
responseBytes: arguments.responseBytes,
|
|
businessId: structKeyExists(request, "BusinessID") ? val(request.BusinessID) : 0,
|
|
userId: structKeyExists(request, "UserID") ? val(request.UserID) : 0,
|
|
loggedAt: dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss")
|
|
};
|
|
|
|
lock name="payfrit_perfBuffer" timeout="1" type="exclusive" {
|
|
arrayAppend(application.perfBuffer, metric);
|
|
if (arrayLen(application.perfBuffer) >= 100) {
|
|
thread name="perfFlush_#createUUID()#" {
|
|
flushPerfBuffer();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// APPLICATION-SCOPE CACHE
|
|
// ============================================
|
|
if (!structKeyExists(application, "cache")) {
|
|
application.cache = {};
|
|
}
|
|
|
|
/**
|
|
* Get a cached value. Returns null if not found or expired.
|
|
* Usage: cached = cacheGet("menu_123", 120);
|
|
* if (!isNull(cached)) { use it } else { query and cachePut }
|
|
*/
|
|
function appCacheGet(required string key, numeric ttlSeconds = 300) {
|
|
if (structKeyExists(application.cache, arguments.key)) {
|
|
var entry = application.cache[arguments.key];
|
|
if (dateDiff("s", entry.cachedAt, now()) < arguments.ttlSeconds) {
|
|
return entry.data;
|
|
}
|
|
structDelete(application.cache, arguments.key);
|
|
}
|
|
return javaCast("null", "");
|
|
}
|
|
|
|
/**
|
|
* Store a value in cache.
|
|
*/
|
|
function appCachePut(required string key, required any data) {
|
|
application.cache[arguments.key] = { data: arguments.data, cachedAt: now() };
|
|
}
|
|
|
|
/**
|
|
* Invalidate all cache keys starting with the given prefix.
|
|
*/
|
|
function appCacheInvalidate(required string keyPrefix) {
|
|
var keys = structKeyArray(application.cache);
|
|
for (var k in keys) {
|
|
if (findNoCase(arguments.keyPrefix, k) == 1) {
|
|
structDelete(application.cache, k);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store in application scope
|
|
application.isDevEnvironment = isDevEnvironment;
|
|
application.isDev = isDev;
|
|
application.logDebug = logDebug;
|
|
application.apiError = apiError;
|
|
</cfscript>
|