Skip to content

Commit 121a7f5

Browse files
authored
Merge pull request #48 from AJFrio/perf/analytics-caching-5466442581751998397
⚡ Performance: Implement caching for AnalyticsService
2 parents 9a34a55 + 09083a3 commit 121a7f5

File tree

3 files changed

+113
-4
lines changed

3 files changed

+113
-4
lines changed

src/routes/admin/analytics.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const router = new Hono()
1111
router.get('/', asyncHandler(async (c) => {
1212
const period = c.req.query('period') || '30d'
1313
const stripeService = new StripeService(c.env.STRIPE_SECRET_KEY, c.env.SITE_URL)
14-
const analyticsService = new AnalyticsService(stripeService)
14+
const kvNamespace = getKVNamespace(c.env)
15+
const analyticsService = new AnalyticsService(stripeService, kvNamespace)
1516
const analytics = await analyticsService.getAnalytics(period)
1617
return c.json(analytics)
1718
}))

src/services/AnalyticsService.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,36 @@
22
import { StripeService } from './StripeService.js'
33

44
export class AnalyticsService {
5-
constructor(stripeService) {
5+
constructor(stripeService, kv = null) {
66
this.stripe = stripeService
7+
this.kv = kv
78
}
89

910
/**
1011
* Get analytics data for a period
1112
*/
1213
async getAnalytics(period = '30d') {
1314
const periodDays = { '1d': 1, '7d': 7, '30d': 30, '90d': 90, '1y': 365 }
14-
const days = periodDays[period] || 30
15+
16+
// Sanitize period to prevent cache poisoning
17+
if (!periodDays[period]) {
18+
period = '30d'
19+
}
20+
21+
// Check cache
22+
if (this.kv) {
23+
const cacheKey = `analytics:${period}`
24+
const cached = await this.kv.get(cacheKey)
25+
if (cached) {
26+
try {
27+
return JSON.parse(cached)
28+
} catch (e) {
29+
console.error('Error parsing cached analytics', e)
30+
}
31+
}
32+
}
33+
34+
const days = periodDays[period]
1535
const now = new Date()
1636
const startDate = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000))
1737

@@ -76,7 +96,7 @@ export class AnalyticsService {
7696
}
7797
}
7898

79-
return {
99+
const result = {
80100
period,
81101
totalRevenue: Math.round(totalRevenue * 100) / 100,
82102
totalOrders,
@@ -88,6 +108,15 @@ export class AnalyticsService {
88108
end: now.toISOString()
89109
}
90110
}
111+
112+
// Cache result
113+
if (this.kv) {
114+
const cacheKey = `analytics:${period}`
115+
// Cache for 5 minutes
116+
await this.kv.put(cacheKey, JSON.stringify(result), { expirationTtl: 300 })
117+
}
118+
119+
return result
91120
}
92121

93122
/**
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { AnalyticsService } from '../../src/services/AnalyticsService.js'
3+
4+
// Mock KV
5+
class MockKV {
6+
constructor() {
7+
this.store = new Map()
8+
}
9+
async get(key) {
10+
return this.store.get(key)
11+
}
12+
async put(key, value) {
13+
this.store.set(key, value)
14+
}
15+
}
16+
17+
// Mock Stripe Service
18+
class MockStripeService {
19+
constructor() {
20+
this.stripe = {
21+
paymentIntents: {
22+
list: async () => ({ data: [] })
23+
}
24+
}
25+
}
26+
27+
async listPaymentIntents(startDate) {
28+
// Simulate network delay
29+
await new Promise(resolve => setTimeout(resolve, 50))
30+
return {
31+
data: Array(10).fill(0).map((_, i) => ({
32+
id: `pi_${i}`,
33+
status: 'succeeded',
34+
amount: 1000,
35+
created: Math.floor(Date.now() / 1000)
36+
}))
37+
}
38+
}
39+
}
40+
41+
describe('AnalyticsService Performance', () => {
42+
let stripeService
43+
let analyticsService
44+
let kv
45+
46+
beforeEach(() => {
47+
vi.clearAllMocks()
48+
stripeService = new MockStripeService()
49+
kv = new MockKV()
50+
51+
// We spy on the method to count calls
52+
vi.spyOn(stripeService, 'listPaymentIntents')
53+
54+
// Initialize service
55+
// Note: kv is passed but currently ignored by AnalyticsService
56+
analyticsService = new AnalyticsService(stripeService, kv)
57+
})
58+
59+
it('should verify cache behavior', async () => {
60+
console.log('--- Starting Performance Test ---')
61+
62+
const start1 = performance.now()
63+
await analyticsService.getAnalytics('30d')
64+
const end1 = performance.now()
65+
const time1 = end1 - start1
66+
67+
const start2 = performance.now()
68+
await analyticsService.getAnalytics('30d')
69+
const end2 = performance.now()
70+
const time2 = end2 - start2
71+
72+
console.log(`Call 1 time: ${time1.toFixed(2)}ms`)
73+
console.log(`Call 2 time: ${time2.toFixed(2)}ms`)
74+
75+
// With caching, the first call hits Stripe, the second hits cache.
76+
// So we expect 1 call to Stripe.
77+
expect(stripeService.listPaymentIntents).toHaveBeenCalledTimes(1)
78+
})
79+
})

0 commit comments

Comments
 (0)