@nextsparkjs/plugin-social-media-publisher 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,748 @@
1
+ # Analytics and Reporting
2
+
3
+ ## Overview
4
+
5
+ This guide covers how to leverage the Social Media Publisher plugin's analytics capabilities to track performance, generate reports, and make data-driven decisions for social media management.
6
+
7
+ **Covered Topics:**
8
+ - Account-level analytics
9
+ - Post performance tracking
10
+ - Client reporting
11
+ - Comparative analysis
12
+ - Export and visualization
13
+ - ROI measurement
14
+
15
+ ## Account-Level Analytics
16
+
17
+ ### Instagram Business Insights
18
+
19
+ **Fetch Account Metrics:**
20
+ ```typescript
21
+ import { InstagramAPI } from '@/contents/plugins/social-media-publisher/lib/providers/instagram'
22
+ import { TokenEncryption } from '@/core/lib/oauth/encryption'
23
+
24
+ export async function getInstagramAnalytics(accountId: string) {
25
+ // Get account with encrypted token
26
+ const account = await query(`
27
+ SELECT * FROM "clients_social_platforms"
28
+ WHERE id = $1
29
+ `, [accountId])
30
+
31
+ if (account.rowCount === 0) throw new Error('Account not found')
32
+
33
+ // Decrypt token
34
+ const [encrypted, iv, keyId] = account.rows[0].accessToken.split(':')
35
+ const decryptedToken = await TokenEncryption.decrypt(encrypted, iv, keyId)
36
+
37
+ // Fetch insights from Instagram API
38
+ const insights = await InstagramAPI.getAccountInsights(
39
+ account.rows[0].platformAccountId,
40
+ decryptedToken
41
+ )
42
+
43
+ // Fetch account info
44
+ const info = await InstagramAPI.getAccountInfo(
45
+ account.rows[0].platformAccountId,
46
+ decryptedToken
47
+ )
48
+
49
+ return {
50
+ account: {
51
+ id: account.rows[0].id,
52
+ username: account.rows[0].platformAccountName,
53
+ followersCount: info.followersCount,
54
+ followsCount: info.followsCount,
55
+ mediaCount: info.mediaCount,
56
+ profilePictureUrl: info.profilePictureUrl
57
+ },
58
+ insights: {
59
+ impressions: insights.impressions,
60
+ reach: insights.reach,
61
+ engagement: insights.engagement,
62
+ likes: insights.likes,
63
+ comments: insights.comments,
64
+ saves: insights.saves,
65
+ profileViews: insights.profileViews
66
+ },
67
+ metrics: {
68
+ engagementRate: ((insights.engagement / insights.reach) * 100).toFixed(2),
69
+ averageEngagementPerPost: (insights.engagement / info.mediaCount).toFixed(0),
70
+ savesRate: ((insights.saves / insights.reach) * 100).toFixed(2)
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ **Display in Dashboard:**
77
+ ```typescript
78
+ // app/dashboard/analytics/[accountId]/page.tsx
79
+ export default async function AccountAnalyticsPage({
80
+ params
81
+ }: {
82
+ params: Promise<{ accountId: string }>
83
+ }) {
84
+ const { accountId } = await params
85
+ const analytics = await getInstagramAnalytics(accountId)
86
+
87
+ return (
88
+ <div className="max-w-6xl mx-auto p-6">
89
+ {/* Header */}
90
+ <div className="flex items-center gap-4 mb-8">
91
+ <img
92
+ src={analytics.account.profilePictureUrl}
93
+ alt={analytics.account.username}
94
+ className="w-20 h-20 rounded-full"
95
+ />
96
+ <div>
97
+ <h1 className="text-2xl font-bold">@{analytics.account.username}</h1>
98
+ <p className="text-gray-600">
99
+ {analytics.account.followersCount.toLocaleString()} followers
100
+ </p>
101
+ </div>
102
+ </div>
103
+
104
+ {/* Metrics Grid */}
105
+ <div className="grid grid-cols-4 gap-4 mb-8">
106
+ <MetricCard
107
+ title="Total Impressions"
108
+ value={analytics.insights.impressions.toLocaleString()}
109
+ icon="👁️"
110
+ />
111
+ <MetricCard
112
+ title="Reach"
113
+ value={analytics.insights.reach.toLocaleString()}
114
+ icon="📊"
115
+ />
116
+ <MetricCard
117
+ title="Engagement"
118
+ value={analytics.insights.engagement.toLocaleString()}
119
+ icon="❤️"
120
+ />
121
+ <MetricCard
122
+ title="Engagement Rate"
123
+ value={`${analytics.metrics.engagementRate}%`}
124
+ icon="📈"
125
+ />
126
+ </div>
127
+
128
+ {/* Detailed Metrics */}
129
+ <div className="grid grid-cols-2 gap-6">
130
+ <Card>
131
+ <CardHeader>Engagement Breakdown</CardHeader>
132
+ <CardContent>
133
+ <div className="space-y-3">
134
+ <MetricRow label="Likes" value={analytics.insights.likes} />
135
+ <MetricRow label="Comments" value={analytics.insights.comments} />
136
+ <MetricRow label="Saves" value={analytics.insights.saves} />
137
+ <MetricRow label="Profile Views" value={analytics.insights.profileViews} />
138
+ </div>
139
+ </CardContent>
140
+ </Card>
141
+
142
+ <Card>
143
+ <CardHeader>Performance Metrics</CardHeader>
144
+ <CardContent>
145
+ <div className="space-y-3">
146
+ <MetricRow
147
+ label="Engagement Rate"
148
+ value={`${analytics.metrics.engagementRate}%`}
149
+ />
150
+ <MetricRow
151
+ label="Avg. Engagement/Post"
152
+ value={analytics.metrics.averageEngagementPerPost}
153
+ />
154
+ <MetricRow
155
+ label="Saves Rate"
156
+ value={`${analytics.metrics.savesRate}%`}
157
+ />
158
+ <MetricRow
159
+ label="Total Posts"
160
+ value={analytics.account.mediaCount}
161
+ />
162
+ </div>
163
+ </CardContent>
164
+ </Card>
165
+ </div>
166
+ </div>
167
+ )
168
+ }
169
+
170
+ function MetricCard({ title, value, icon }: {
171
+ title: string
172
+ value: string
173
+ icon: string
174
+ }) {
175
+ return (
176
+ <div className="border rounded-lg p-4">
177
+ <div className="text-2xl mb-2">{icon}</div>
178
+ <p className="text-sm text-gray-600">{title}</p>
179
+ <p className="text-2xl font-bold">{value}</p>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ function MetricRow({ label, value }: { label: string; value: number | string }) {
185
+ return (
186
+ <div className="flex justify-between items-center">
187
+ <span className="text-gray-600">{label}</span>
188
+ <span className="font-semibold">{value}</span>
189
+ </div>
190
+ )
191
+ }
192
+ ```
193
+
194
+ ### Facebook Page Insights
195
+
196
+ **Similar to Instagram but with Page-specific metrics:**
197
+ ```typescript
198
+ export async function getFacebookPageAnalytics(accountId: string) {
199
+ // Similar structure to Instagram
200
+ const account = await getAccount(accountId)
201
+ const decryptedToken = await decryptToken(account.accessToken)
202
+
203
+ const insights = await FacebookAPI.getPageInsights(
204
+ account.platformAccountId,
205
+ decryptedToken
206
+ )
207
+
208
+ const pageInfo = await FacebookAPI.getPageInfo(
209
+ account.platformAccountId,
210
+ decryptedToken
211
+ )
212
+
213
+ return {
214
+ account: {
215
+ id: account.id,
216
+ name: account.platformAccountName,
217
+ fanCount: pageInfo.fanCount,
218
+ about: pageInfo.about,
219
+ link: pageInfo.link
220
+ },
221
+ insights: {
222
+ impressions: insights.impressions,
223
+ reach: insights.reach,
224
+ engagement: insights.engagement,
225
+ reactions: insights.reactions,
226
+ comments: insights.comments,
227
+ shares: insights.shares
228
+ }
229
+ }
230
+ }
231
+ ```
232
+
233
+ ## Post Performance Tracking
234
+
235
+ ### Individual Post Analytics
236
+
237
+ **Track Post Performance from Audit Logs:**
238
+ ```typescript
239
+ export async function getPostPerformance(clientId: string, days: number = 30) {
240
+ // Get all published posts from audit logs
241
+ const posts = await query(`
242
+ SELECT
243
+ al.id,
244
+ al."createdAt" as "publishedAt",
245
+ al.details->>'postId' as "postId",
246
+ al.details->>'postUrl' as "postUrl",
247
+ al.details->>'platform' as platform,
248
+ al.details->>'accountName' as "accountName",
249
+ al.details->>'caption' as caption,
250
+ al.details->>'imageUrl' as "imageUrl",
251
+ al."accountId"
252
+ FROM "audit_logs" al
253
+ JOIN "clients_social_platforms" csp ON csp.id = al."accountId"
254
+ WHERE csp."parentId" = $1
255
+ AND al.action = 'post_published'
256
+ AND al."createdAt" > NOW() - INTERVAL '${days} days'
257
+ ORDER BY al."createdAt" DESC
258
+ `, [clientId])
259
+
260
+ // Enrich with real-time insights from APIs
261
+ const enrichedPosts = await Promise.all(
262
+ posts.rows.map(async (post) => {
263
+ try {
264
+ const account = await getAccount(post.accountId)
265
+ const token = await decryptToken(account.accessToken)
266
+
267
+ let insights = {}
268
+
269
+ if (post.platform === 'instagram_business') {
270
+ insights = await InstagramAPI.getMediaInsights(post.postId, token)
271
+ } else {
272
+ // Facebook post insights
273
+ // Note: Requires additional API implementation
274
+ }
275
+
276
+ return {
277
+ ...post,
278
+ insights
279
+ }
280
+ } catch (error) {
281
+ return {
282
+ ...post,
283
+ insights: null,
284
+ error: 'Failed to fetch insights'
285
+ }
286
+ }
287
+ })
288
+ )
289
+
290
+ return enrichedPosts
291
+ }
292
+ ```
293
+
294
+ **Display Top Performing Posts:**
295
+ ```typescript
296
+ // app/dashboard/analytics/top-posts/page.tsx
297
+ export default async function TopPostsPage({
298
+ searchParams
299
+ }: {
300
+ searchParams: Promise<{ clientId?: string; days?: string }>
301
+ }) {
302
+ const params = await searchParams
303
+ const clientId = params.clientId
304
+ const days = parseInt(params.days || '30')
305
+
306
+ const posts = await getPostPerformance(clientId, days)
307
+
308
+ // Sort by engagement
309
+ const sortedPosts = posts
310
+ .filter(p => p.insights)
311
+ .sort((a, b) => (b.insights.engagement || 0) - (a.insights.engagement || 0))
312
+
313
+ return (
314
+ <div className="max-w-6xl mx-auto p-6">
315
+ <h1 className="text-2xl font-bold mb-6">
316
+ Top Performing Posts ({days} days)
317
+ </h1>
318
+
319
+ <div className="grid grid-cols-1 gap-4">
320
+ {sortedPosts.slice(0, 10).map((post, index) => (
321
+ <div key={post.id} className="border rounded-lg p-4 flex gap-4">
322
+ {/* Rank */}
323
+ <div className="text-3xl font-bold text-gray-300 w-12">
324
+ #{index + 1}
325
+ </div>
326
+
327
+ {/* Image */}
328
+ <img
329
+ src={post.imageUrl}
330
+ alt=""
331
+ className="w-32 h-32 object-cover rounded"
332
+ />
333
+
334
+ {/* Content */}
335
+ <div className="flex-1">
336
+ <div className="flex justify-between items-start">
337
+ <div>
338
+ <p className="font-medium">{post.accountName}</p>
339
+ <p className="text-sm text-gray-600">
340
+ {new Date(post.publishedAt).toLocaleDateString()}
341
+ </p>
342
+ </div>
343
+ <a
344
+ href={post.postUrl}
345
+ target="_blank"
346
+ className="text-blue-600 text-sm"
347
+ >
348
+ View Post →
349
+ </a>
350
+ </div>
351
+
352
+ <p className="text-sm mt-2 line-clamp-2">{post.caption}</p>
353
+
354
+ {/* Metrics */}
355
+ <div className="flex gap-6 mt-3">
356
+ <Metric
357
+ label="Impressions"
358
+ value={post.insights.impressions}
359
+ />
360
+ <Metric
361
+ label="Engagement"
362
+ value={post.insights.engagement}
363
+ />
364
+ <Metric
365
+ label="Likes"
366
+ value={post.insights.likes}
367
+ />
368
+ <Metric
369
+ label="Comments"
370
+ value={post.insights.comments}
371
+ />
372
+ <Metric
373
+ label="Saves"
374
+ value={post.insights.saves}
375
+ />
376
+ </div>
377
+ </div>
378
+ </div>
379
+ ))}
380
+ </div>
381
+ </div>
382
+ )
383
+ }
384
+
385
+ function Metric({ label, value }: { label: string; value: number }) {
386
+ return (
387
+ <div>
388
+ <p className="text-xs text-gray-600">{label}</p>
389
+ <p className="font-semibold">{value?.toLocaleString() || '0'}</p>
390
+ </div>
391
+ )
392
+ }
393
+ ```
394
+
395
+ ## Client Reporting
396
+
397
+ ### Monthly Report Generation
398
+
399
+ **Comprehensive Client Report:**
400
+ ```typescript
401
+ export async function generateMonthlyReport(
402
+ clientId: string,
403
+ year: number,
404
+ month: number
405
+ ) {
406
+ // Date range
407
+ const startDate = new Date(year, month - 1, 1)
408
+ const endDate = new Date(year, month, 0)
409
+
410
+ // Get client info
411
+ const client = await query(`
412
+ SELECT * FROM "clients" WHERE id = $1
413
+ `, [clientId])
414
+
415
+ // Get all accounts
416
+ const accounts = await query(`
417
+ SELECT * FROM "clients_social_platforms"
418
+ WHERE "parentId" = $1 AND "isActive" = true
419
+ `, [clientId])
420
+
421
+ // Get publishing activity
422
+ const publishingStats = await query(`
423
+ SELECT
424
+ al.details->>'platform' as platform,
425
+ COUNT(*) FILTER (WHERE al.action = 'post_published') as successful,
426
+ COUNT(*) FILTER (WHERE al.action = 'post_failed') as failed
427
+ FROM "audit_logs" al
428
+ WHERE al."accountId" = ANY($1)
429
+ AND al."createdAt" BETWEEN $2 AND $3
430
+ GROUP BY al.details->>'platform'
431
+ `, [accounts.rows.map(a => a.id), startDate, endDate])
432
+
433
+ // Fetch insights for each account
434
+ const accountInsights = await Promise.all(
435
+ accounts.rows.map(async (account) => {
436
+ const token = await decryptToken(account.accessToken)
437
+
438
+ try {
439
+ if (account.platform === 'instagram_business') {
440
+ const insights = await InstagramAPI.getAccountInsights(
441
+ account.platformAccountId,
442
+ token
443
+ )
444
+ const info = await InstagramAPI.getAccountInfo(
445
+ account.platformAccountId,
446
+ token
447
+ )
448
+
449
+ return {
450
+ accountName: account.platformAccountName,
451
+ platform: account.platform,
452
+ followersCount: info.followersCount,
453
+ ...insights
454
+ }
455
+ } else {
456
+ const insights = await FacebookAPI.getPageInsights(
457
+ account.platformAccountId,
458
+ token
459
+ )
460
+ const info = await FacebookAPI.getPageInfo(
461
+ account.platformAccountId,
462
+ token
463
+ )
464
+
465
+ return {
466
+ accountName: account.platformAccountName,
467
+ platform: account.platform,
468
+ fanCount: info.fanCount,
469
+ ...insights
470
+ }
471
+ }
472
+ } catch (error) {
473
+ return {
474
+ accountName: account.platformAccountName,
475
+ platform: account.platform,
476
+ error: 'Failed to fetch insights'
477
+ }
478
+ }
479
+ })
480
+ )
481
+
482
+ // Calculate totals
483
+ const totals = accountInsights.reduce(
484
+ (acc, curr) => {
485
+ if (!curr.error) {
486
+ acc.impressions += curr.impressions || 0
487
+ acc.reach += curr.reach || 0
488
+ acc.engagement += curr.engagement || 0
489
+ }
490
+ return acc
491
+ },
492
+ { impressions: 0, reach: 0, engagement: 0 }
493
+ )
494
+
495
+ const totalPosts = publishingStats.rows.reduce(
496
+ (sum, row) => sum + row.successful,
497
+ 0
498
+ )
499
+
500
+ return {
501
+ client: client.rows[0],
502
+ period: {
503
+ month,
504
+ year,
505
+ startDate,
506
+ endDate
507
+ },
508
+ summary: {
509
+ totalPosts,
510
+ totalAccounts: accounts.rowCount,
511
+ totalImpressions: totals.impressions,
512
+ totalReach: totals.reach,
513
+ totalEngagement: totals.engagement,
514
+ engagementRate: ((totals.engagement / totals.reach) * 100).toFixed(2)
515
+ },
516
+ accounts: accountInsights,
517
+ publishingStats: publishingStats.rows
518
+ }
519
+ }
520
+ ```
521
+
522
+ **Export as PDF:**
523
+ ```typescript
524
+ import { jsPDF } from 'jspdf'
525
+
526
+ export async function exportReportAsPDF(clientId: string, month: number, year: number) {
527
+ const report = await generateMonthlyReport(clientId, year, month)
528
+
529
+ const doc = new jsPDF()
530
+ const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
531
+ 'July', 'August', 'September', 'October', 'November', 'December']
532
+
533
+ // Title Page
534
+ doc.setFontSize(24)
535
+ doc.text(report.client.name, 20, 30)
536
+ doc.setFontSize(18)
537
+ doc.text(`Social Media Report`, 20, 45)
538
+ doc.setFontSize(14)
539
+ doc.text(`${monthNames[month - 1]} ${year}`, 20, 55)
540
+
541
+ // Summary
542
+ doc.addPage()
543
+ doc.setFontSize(16)
544
+ doc.text('Executive Summary', 20, 30)
545
+
546
+ doc.setFontSize(12)
547
+ let y = 50
548
+ doc.text(`Total Posts Published: ${report.summary.totalPosts}`, 20, y)
549
+ y += 10
550
+ doc.text(`Total Impressions: ${report.summary.totalImpressions.toLocaleString()}`, 20, y)
551
+ y += 10
552
+ doc.text(`Total Reach: ${report.summary.totalReach.toLocaleString()}`, 20, y)
553
+ y += 10
554
+ doc.text(`Total Engagement: ${report.summary.totalEngagement.toLocaleString()}`, 20, y)
555
+ y += 10
556
+ doc.text(`Engagement Rate: ${report.summary.engagementRate}%`, 20, y)
557
+
558
+ // Account Breakdown
559
+ doc.addPage()
560
+ doc.setFontSize(16)
561
+ doc.text('Account Performance', 20, 30)
562
+
563
+ y = 50
564
+ report.accounts.forEach((account) => {
565
+ if (account.error) return
566
+
567
+ doc.setFontSize(14)
568
+ doc.text(`${account.accountName}`, 20, y)
569
+
570
+ doc.setFontSize(11)
571
+ y += 10
572
+ doc.text(`Platform: ${account.platform === 'instagram_business' ? 'Instagram' : 'Facebook'}`, 30, y)
573
+ y += 8
574
+ doc.text(`Followers: ${(account.followersCount || account.fanCount || 0).toLocaleString()}`, 30, y)
575
+ y += 8
576
+ doc.text(`Impressions: ${(account.impressions || 0).toLocaleString()}`, 30, y)
577
+ y += 8
578
+ doc.text(`Engagement: ${(account.engagement || 0).toLocaleString()}`, 30, y)
579
+ y += 15
580
+
581
+ if (y > 250) {
582
+ doc.addPage()
583
+ y = 30
584
+ }
585
+ })
586
+
587
+ // Save
588
+ doc.save(`${report.client.slug}-report-${year}-${month}.pdf`)
589
+ }
590
+ ```
591
+
592
+ ## Comparative Analysis
593
+
594
+ ### Month-over-Month Comparison
595
+
596
+ ```typescript
597
+ export async function getComparativeAnalytics(clientId: string) {
598
+ const currentMonth = new Date().getMonth() + 1
599
+ const currentYear = new Date().getFullYear()
600
+ const previousMonth = currentMonth === 1 ? 12 : currentMonth - 1
601
+ const previousYear = currentMonth === 1 ? currentYear - 1 : currentYear
602
+
603
+ const [current, previous] = await Promise.all([
604
+ generateMonthlyReport(clientId, currentYear, currentMonth),
605
+ generateMonthlyReport(clientId, previousYear, previousMonth)
606
+ ])
607
+
608
+ return {
609
+ current: current.summary,
610
+ previous: previous.summary,
611
+ changes: {
612
+ posts: calculateChange(current.summary.totalPosts, previous.summary.totalPosts),
613
+ impressions: calculateChange(current.summary.totalImpressions, previous.summary.totalImpressions),
614
+ reach: calculateChange(current.summary.totalReach, previous.summary.totalReach),
615
+ engagement: calculateChange(current.summary.totalEngagement, previous.summary.totalEngagement),
616
+ engagementRate: calculateChange(
617
+ parseFloat(current.summary.engagementRate),
618
+ parseFloat(previous.summary.engagementRate)
619
+ )
620
+ }
621
+ }
622
+ }
623
+
624
+ function calculateChange(current: number, previous: number) {
625
+ if (previous === 0) return { value: 0, percentage: 0 }
626
+
627
+ const difference = current - previous
628
+ const percentage = ((difference / previous) * 100).toFixed(1)
629
+
630
+ return {
631
+ value: difference,
632
+ percentage: parseFloat(percentage),
633
+ isPositive: difference >= 0
634
+ }
635
+ }
636
+ ```
637
+
638
+ **Display Comparison:**
639
+ ```typescript
640
+ export function ComparativeAnalyticsDashboard({ data }: { data: any }) {
641
+ return (
642
+ <div className="grid grid-cols-2 gap-6">
643
+ {/* Current Month */}
644
+ <div>
645
+ <h3 className="font-medium mb-4">This Month</h3>
646
+ <div className="space-y-3">
647
+ <MetricWithChange
648
+ label="Posts"
649
+ current={data.current.totalPosts}
650
+ change={data.changes.posts}
651
+ />
652
+ <MetricWithChange
653
+ label="Impressions"
654
+ current={data.current.totalImpressions}
655
+ change={data.changes.impressions}
656
+ />
657
+ <MetricWithChange
658
+ label="Engagement"
659
+ current={data.current.totalEngagement}
660
+ change={data.changes.engagement}
661
+ />
662
+ <MetricWithChange
663
+ label="Engagement Rate"
664
+ current={`${data.current.engagementRate}%`}
665
+ change={data.changes.engagementRate}
666
+ />
667
+ </div>
668
+ </div>
669
+
670
+ {/* Previous Month */}
671
+ <div>
672
+ <h3 className="font-medium mb-4">Last Month</h3>
673
+ <div className="space-y-3">
674
+ <SimplMetric label="Posts" value={data.previous.totalPosts} />
675
+ <SimpleMetric label="Impressions" value={data.previous.totalImpressions} />
676
+ <SimpleMetric label="Engagement" value={data.previous.totalEngagement} />
677
+ <SimpleMetric label="Engagement Rate" value={`${data.previous.engagementRate}%`} />
678
+ </div>
679
+ </div>
680
+ </div>
681
+ )
682
+ }
683
+
684
+ function MetricWithChange({ label, current, change }: {
685
+ label: string
686
+ current: number | string
687
+ change: { value: number; percentage: number; isPositive: boolean }
688
+ }) {
689
+ return (
690
+ <div className="border rounded p-3">
691
+ <p className="text-sm text-gray-600">{label}</p>
692
+ <div className="flex justify-between items-end">
693
+ <p className="text-2xl font-bold">{current}</p>
694
+ <div className={`text-sm ${change.isPositive ? 'text-green-600' : 'text-red-600'}`}>
695
+ {change.isPositive ? '↑' : '↓'} {Math.abs(change.percentage)}%
696
+ </div>
697
+ </div>
698
+ </div>
699
+ )
700
+ }
701
+ ```
702
+
703
+ ## Best Practices
704
+
705
+ ### Data Refresh Strategy
706
+
707
+ ```typescript
708
+ // Cache insights for 24 hours
709
+ export async function getCachedInsights(accountId: string) {
710
+ const cacheKey = `insights:${accountId}`
711
+
712
+ // Check cache
713
+ const cached = await redis.get(cacheKey)
714
+ if (cached) return JSON.parse(cached)
715
+
716
+ // Fetch fresh data
717
+ const insights = await getInstagramAnalytics(accountId)
718
+
719
+ // Cache for 24 hours
720
+ await redis.setex(cacheKey, 86400, JSON.stringify(insights))
721
+
722
+ return insights
723
+ }
724
+ ```
725
+
726
+ ### Rate Limit Handling
727
+
728
+ ```typescript
729
+ // Batch insights fetching to avoid rate limits
730
+ export async function batchFetchInsights(accountIds: string[]) {
731
+ const results = []
732
+
733
+ for (const accountId of accountIds) {
734
+ results.push(await getCachedInsights(accountId))
735
+
736
+ // Wait 1 second between requests
737
+ await new Promise(resolve => setTimeout(resolve, 1000))
738
+ }
739
+
740
+ return results
741
+ }
742
+ ```
743
+
744
+ ## Next Steps
745
+
746
+ - **[Agency Management](./01-agency-management.md)** - Multi-client workflows
747
+ - **[Content Publishing](./02-content-publishing.md)** - Publishing workflows
748
+ - **[Provider APIs](../03-advanced-usage/01-provider-apis.md)** - API reference