diff --git a/src/routes/admin/analytics.js b/src/routes/admin/analytics.js index 68c3f5b..64143a5 100644 --- a/src/routes/admin/analytics.js +++ b/src/routes/admin/analytics.js @@ -11,7 +11,8 @@ const router = new Hono() router.get('/', asyncHandler(async (c) => { const period = c.req.query('period') || '30d' const stripeService = new StripeService(c.env.STRIPE_SECRET_KEY, c.env.SITE_URL) - const analyticsService = new AnalyticsService(stripeService) + const kvNamespace = getKVNamespace(c.env) + const analyticsService = new AnalyticsService(stripeService, kvNamespace) const analytics = await analyticsService.getAnalytics(period) return c.json(analytics) })) diff --git a/src/services/AnalyticsService.js b/src/services/AnalyticsService.js index 86a6da6..89e006e 100644 --- a/src/services/AnalyticsService.js +++ b/src/services/AnalyticsService.js @@ -2,8 +2,9 @@ import { StripeService } from './StripeService.js' export class AnalyticsService { - constructor(stripeService) { + constructor(stripeService, kv = null) { this.stripe = stripeService + this.kv = kv } /** @@ -11,7 +12,26 @@ export class AnalyticsService { */ async getAnalytics(period = '30d') { const periodDays = { '1d': 1, '7d': 7, '30d': 30, '90d': 90, '1y': 365 } - const days = periodDays[period] || 30 + + // Sanitize period to prevent cache poisoning + if (!periodDays[period]) { + period = '30d' + } + + // Check cache + if (this.kv) { + const cacheKey = `analytics:${period}` + const cached = await this.kv.get(cacheKey) + if (cached) { + try { + return JSON.parse(cached) + } catch (e) { + console.error('Error parsing cached analytics', e) + } + } + } + + const days = periodDays[period] const now = new Date() const startDate = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000)) @@ -76,7 +96,7 @@ export class AnalyticsService { } } - return { + const result = { period, totalRevenue: Math.round(totalRevenue * 100) / 100, totalOrders, @@ -88,6 +108,15 @@ export class AnalyticsService { end: now.toISOString() } } + + // Cache result + if (this.kv) { + const cacheKey = `analytics:${period}` + // Cache for 5 minutes + await this.kv.put(cacheKey, JSON.stringify(result), { expirationTtl: 300 }) + } + + return result } /** diff --git a/tests/services/AnalyticsService.perf.test.js b/tests/services/AnalyticsService.perf.test.js new file mode 100644 index 0000000..dc66efd --- /dev/null +++ b/tests/services/AnalyticsService.perf.test.js @@ -0,0 +1,79 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { AnalyticsService } from '../../src/services/AnalyticsService.js' + +// Mock KV +class MockKV { + constructor() { + this.store = new Map() + } + async get(key) { + return this.store.get(key) + } + async put(key, value) { + this.store.set(key, value) + } +} + +// Mock Stripe Service +class MockStripeService { + constructor() { + this.stripe = { + paymentIntents: { + list: async () => ({ data: [] }) + } + } + } + + async listPaymentIntents(startDate) { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 50)) + return { + data: Array(10).fill(0).map((_, i) => ({ + id: `pi_${i}`, + status: 'succeeded', + amount: 1000, + created: Math.floor(Date.now() / 1000) + })) + } + } +} + +describe('AnalyticsService Performance', () => { + let stripeService + let analyticsService + let kv + + beforeEach(() => { + vi.clearAllMocks() + stripeService = new MockStripeService() + kv = new MockKV() + + // We spy on the method to count calls + vi.spyOn(stripeService, 'listPaymentIntents') + + // Initialize service + // Note: kv is passed but currently ignored by AnalyticsService + analyticsService = new AnalyticsService(stripeService, kv) + }) + + it('should verify cache behavior', async () => { + console.log('--- Starting Performance Test ---') + + const start1 = performance.now() + await analyticsService.getAnalytics('30d') + const end1 = performance.now() + const time1 = end1 - start1 + + const start2 = performance.now() + await analyticsService.getAnalytics('30d') + const end2 = performance.now() + const time2 = end2 - start2 + + console.log(`Call 1 time: ${time1.toFixed(2)}ms`) + console.log(`Call 2 time: ${time2.toFixed(2)}ms`) + + // With caching, the first call hits Stripe, the second hits cache. + // So we expect 1 call to Stripe. + expect(stripeService.listPaymentIntents).toHaveBeenCalledTimes(1) + }) +})