This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/config/environment.cfm
John Mizerek dc9db32b58 Add API performance profiling, caching, and query optimizations
- 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>
2026-01-29 20:41:27 -08:00

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>