posterly-mcp-server 0.19.4 → 0.19.5
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.
- package/README.md +1 -1
- package/dist/lib/api-client.d.ts +2 -0
- package/dist/tools/get-account-analytics.d.ts +7 -0
- package/dist/tools/get-account-analytics.js +121 -56
- package/dist/tools/get-post-analytics.d.ts +7 -0
- package/dist/tools/get-post-analytics.js +133 -54
- package/package.json +1 -1
- package/src/lib/api-client.ts +1 -1
- package/src/tools/get-account-analytics.ts +127 -64
- package/src/tools/get-post-analytics.ts +139 -57
package/README.md
CHANGED
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -165,8 +165,10 @@ export interface AccountAnalyticsSummary {
|
|
|
165
165
|
total_likes?: number | null;
|
|
166
166
|
platform_metrics?: Record<string, number | null | undefined>;
|
|
167
167
|
display_metrics?: Array<{
|
|
168
|
+
key?: string;
|
|
168
169
|
label: string;
|
|
169
170
|
value: number | null | undefined;
|
|
171
|
+
unit?: string;
|
|
170
172
|
}>;
|
|
171
173
|
}
|
|
172
174
|
export interface AccountAnalyticsSnapshot {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
declare const presentationSchema: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
|
|
4
|
+
type AnalyticsPresentation = z.infer<typeof presentationSchema>;
|
|
3
5
|
export declare const getAccountAnalyticsTool: {
|
|
4
6
|
name: string;
|
|
5
7
|
description: string;
|
|
@@ -7,18 +9,23 @@ export declare const getAccountAnalyticsTool: {
|
|
|
7
9
|
account_id: z.ZodNumber;
|
|
8
10
|
from: z.ZodOptional<z.ZodString>;
|
|
9
11
|
to: z.ZodOptional<z.ZodString>;
|
|
12
|
+
presentation: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
|
|
10
13
|
}, "strip", z.ZodTypeAny, {
|
|
11
14
|
account_id: number;
|
|
12
15
|
from?: string | undefined;
|
|
13
16
|
to?: string | undefined;
|
|
17
|
+
presentation?: "compact" | "table" | "json" | undefined;
|
|
14
18
|
}, {
|
|
15
19
|
account_id: number;
|
|
16
20
|
from?: string | undefined;
|
|
17
21
|
to?: string | undefined;
|
|
22
|
+
presentation?: "compact" | "table" | "json" | undefined;
|
|
18
23
|
}>;
|
|
19
24
|
execute(client: PosterlyClient, input: {
|
|
20
25
|
account_id: number;
|
|
21
26
|
from?: string;
|
|
22
27
|
to?: string;
|
|
28
|
+
presentation?: AnalyticsPresentation;
|
|
23
29
|
}): Promise<string>;
|
|
24
30
|
};
|
|
31
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
|
|
2
3
|
export const getAccountAnalyticsTool = {
|
|
3
4
|
name: 'get_account_analytics',
|
|
4
|
-
description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels
|
|
5
|
+
description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels and supports presentation: compact, table, or json.',
|
|
5
6
|
inputSchema: z.object({
|
|
6
7
|
account_id: z
|
|
7
8
|
.number()
|
|
@@ -14,74 +15,138 @@ export const getAccountAnalyticsTool = {
|
|
|
14
15
|
.string()
|
|
15
16
|
.optional()
|
|
16
17
|
.describe('End date (ISO date). Defaults to today.'),
|
|
18
|
+
presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
|
|
17
19
|
}),
|
|
18
20
|
async execute(client, input) {
|
|
19
21
|
const result = await client.getAccountAnalytics(input);
|
|
20
|
-
|
|
21
|
-
const accountLabel = account.platform === 'google_business'
|
|
22
|
-
? `${account.username} (Google Business, id ${account.id})`
|
|
23
|
-
: `@${account.username} (${account.platform}, id ${account.id})`;
|
|
24
|
-
const lines = [
|
|
25
|
-
`Analytics for ${accountLabel}`,
|
|
26
|
-
`Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
|
|
27
|
-
'',
|
|
28
|
-
'Summary:',
|
|
29
|
-
];
|
|
30
|
-
const nativeRows = getNativeMetricRows(account.platform, summary);
|
|
31
|
-
if (nativeRows.length > 0) {
|
|
32
|
-
for (const [label, value] of nativeRows) {
|
|
33
|
-
pushMetric(lines, label, value);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
else if (account.platform === 'pinterest') {
|
|
37
|
-
pushGenericSummary(lines, summary);
|
|
38
|
-
pushMetric(lines, 'Outbound clicks', summary.total_website_clicks);
|
|
39
|
-
}
|
|
40
|
-
else if (account.platform === 'youtube') {
|
|
41
|
-
pushGenericSummary(lines, summary);
|
|
42
|
-
pushMetric(lines, 'Watch minutes', summary.total_watch_minutes);
|
|
43
|
-
pushMetric(lines, 'Likes', summary.total_likes);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
pushGenericSummary(lines, summary);
|
|
47
|
-
}
|
|
48
|
-
return lines.join('\n');
|
|
22
|
+
return formatAccountAnalytics(result, input.presentation || 'compact');
|
|
49
23
|
},
|
|
50
24
|
};
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
25
|
+
function formatAccountAnalytics(result, presentation) {
|
|
26
|
+
const { account, range, summary, snapshots } = result;
|
|
27
|
+
const rows = accountMetricRows(summary, account.platform);
|
|
28
|
+
const insights = analyticsInsights(summary, account.platform);
|
|
29
|
+
if (presentation === 'json') {
|
|
30
|
+
return JSON.stringify({
|
|
31
|
+
type: 'account_analytics',
|
|
32
|
+
account,
|
|
33
|
+
range,
|
|
34
|
+
snapshot_count: snapshots.length,
|
|
35
|
+
metrics: rows.map(([label, value]) => ({ label, value })),
|
|
36
|
+
insights,
|
|
37
|
+
summary,
|
|
38
|
+
snapshots,
|
|
39
|
+
}, null, 2);
|
|
40
|
+
}
|
|
41
|
+
if (presentation === 'table') {
|
|
42
|
+
return [
|
|
43
|
+
`**📊 Analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
44
|
+
`_${account.platform} · ${range.from} → ${range.to}_`,
|
|
45
|
+
'',
|
|
46
|
+
'**Account**',
|
|
47
|
+
markdownTable(['Field', 'Value'], [
|
|
48
|
+
['Account ID', String(account.id)],
|
|
49
|
+
['Daily snapshots', formatNumber(snapshots.length)],
|
|
50
|
+
]),
|
|
51
|
+
'',
|
|
52
|
+
'**Metrics**',
|
|
53
|
+
markdownTable(['Metric', 'Value'], rows),
|
|
54
|
+
'',
|
|
55
|
+
'**Quick read**',
|
|
56
|
+
insights.map((insight) => `- ${insight}`).join('\n'),
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
return [
|
|
60
|
+
`**📊 Analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
61
|
+
`_${account.platform} · ${range.from} → ${range.to}_`,
|
|
62
|
+
'',
|
|
63
|
+
'**Account**',
|
|
64
|
+
`- **Account ID:** ${account.id}`,
|
|
65
|
+
`- **Daily snapshots:** ${formatNumber(snapshots.length)}`,
|
|
66
|
+
'',
|
|
67
|
+
'**Key metrics**',
|
|
68
|
+
rows.map(([label, value]) => `- **${label}:** ${value}`).join('\n'),
|
|
69
|
+
'',
|
|
70
|
+
'**Quick read**',
|
|
71
|
+
insights.map((insight) => `- ${insight}`).join('\n'),
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
function accountMetricRows(summary, platform) {
|
|
75
|
+
const nativeRows = nativeMetricRows(summary);
|
|
76
|
+
if (nativeRows.length > 0)
|
|
77
|
+
return nativeRows;
|
|
60
78
|
if (platform === 'google_business') {
|
|
61
79
|
const metrics = summary.platform_metrics || {};
|
|
62
80
|
return [
|
|
63
|
-
['Profile Views', metrics.profile_views ?? summary.total_views],
|
|
64
|
-
['Search Views', metrics.search_views ?? summary.total_reach],
|
|
65
|
-
['Maps Views', metrics.maps_views ?? summary.total_profile_views],
|
|
66
|
-
['Customer Actions', metrics.customer_actions ?? summary.total_accounts_engaged],
|
|
67
|
-
['Posts', metrics.posts ?? summary.current_media_count],
|
|
81
|
+
['Profile Views', formatNumber(metrics.profile_views ?? summary.total_views)],
|
|
82
|
+
['Search Views', formatNumber(metrics.search_views ?? summary.total_reach)],
|
|
83
|
+
['Maps Views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
|
|
84
|
+
['Customer Actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
|
|
85
|
+
['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
|
|
68
86
|
];
|
|
69
87
|
}
|
|
70
|
-
|
|
88
|
+
const rows = genericMetricRows(summary);
|
|
89
|
+
if (platform === 'pinterest')
|
|
90
|
+
rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
|
|
91
|
+
if (platform === 'youtube') {
|
|
92
|
+
rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
|
|
93
|
+
rows.push(['Likes', formatNumber(summary.total_likes)]);
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
function nativeMetricRows(summary) {
|
|
98
|
+
return (summary.display_metrics || [])
|
|
99
|
+
.filter((metric) => metric?.label)
|
|
100
|
+
.map((metric) => [metric.label, formatNumber(metric.value)]);
|
|
101
|
+
}
|
|
102
|
+
function genericMetricRows(summary) {
|
|
103
|
+
return [
|
|
104
|
+
['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
|
|
105
|
+
['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
|
|
106
|
+
['Total reach', formatNumber(summary.total_reach)],
|
|
107
|
+
['Total views', formatNumber(summary.total_views)],
|
|
108
|
+
['Profile views', formatNumber(summary.total_profile_views)],
|
|
109
|
+
['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
|
|
110
|
+
['Engagement rate by reach', formatPercent(summary.engagement_rate)],
|
|
111
|
+
['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
function analyticsInsights(summary, platform) {
|
|
115
|
+
const insights = [];
|
|
116
|
+
if (platform === 'google_business' && (summary.display_metrics?.length || 0) > 0) {
|
|
117
|
+
insights.push('Google Business metrics are shown with dashboard-native labels.');
|
|
118
|
+
}
|
|
119
|
+
if ((summary.followers_change || 0) > 0)
|
|
120
|
+
insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
|
|
121
|
+
if ((summary.followers_change || 0) < 0)
|
|
122
|
+
insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
|
|
123
|
+
if (summary.engagement_rate != null && platform !== 'google_business')
|
|
124
|
+
insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
|
|
125
|
+
if (platform === 'youtube' && summary.total_watch_minutes != null)
|
|
126
|
+
insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
|
|
127
|
+
return insights.length ? insights : ['No standout movement detected in the returned period.'];
|
|
71
128
|
}
|
|
72
129
|
function formatDelta(n) {
|
|
73
130
|
return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
|
|
74
131
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
pushMetric(lines, 'Total reach', summary.total_reach);
|
|
78
|
-
pushMetric(lines, 'Total views', summary.total_views);
|
|
79
|
-
pushMetric(lines, 'Profile views', summary.total_profile_views);
|
|
80
|
-
pushMetric(lines, 'Total accounts engaged', summary.total_accounts_engaged);
|
|
81
|
-
lines.push(`• Engagement rate (by reach): ${summary.engagement_rate}%`, `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`);
|
|
132
|
+
function formatNumber(value) {
|
|
133
|
+
return value == null ? 'n/a' : value.toLocaleString();
|
|
82
134
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
function formatPercent(value) {
|
|
136
|
+
return value == null ? 'n/a' : `${formatNumber(value)}%`;
|
|
137
|
+
}
|
|
138
|
+
function accountLabel(username) {
|
|
139
|
+
if (!username)
|
|
140
|
+
return 'account';
|
|
141
|
+
return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
|
|
142
|
+
}
|
|
143
|
+
function markdownTable(headers, rows) {
|
|
144
|
+
return [
|
|
145
|
+
`| ${headers.map(escapeCell).join(' | ')} |`,
|
|
146
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
147
|
+
...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
function escapeCell(value) {
|
|
151
|
+
return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
87
152
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
declare const presentationSchema: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
|
|
4
|
+
type AnalyticsPresentation = z.infer<typeof presentationSchema>;
|
|
3
5
|
export declare const getPostAnalyticsTool: {
|
|
4
6
|
name: string;
|
|
5
7
|
description: string;
|
|
@@ -9,18 +11,21 @@ export declare const getPostAnalyticsTool: {
|
|
|
9
11
|
to: z.ZodOptional<z.ZodString>;
|
|
10
12
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
11
13
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
presentation: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
|
|
12
15
|
}, "strip", z.ZodTypeAny, {
|
|
13
16
|
account_id: number;
|
|
14
17
|
limit?: number | undefined;
|
|
15
18
|
offset?: number | undefined;
|
|
16
19
|
from?: string | undefined;
|
|
17
20
|
to?: string | undefined;
|
|
21
|
+
presentation?: "compact" | "table" | "json" | undefined;
|
|
18
22
|
}, {
|
|
19
23
|
account_id: number;
|
|
20
24
|
limit?: number | undefined;
|
|
21
25
|
offset?: number | undefined;
|
|
22
26
|
from?: string | undefined;
|
|
23
27
|
to?: string | undefined;
|
|
28
|
+
presentation?: "compact" | "table" | "json" | undefined;
|
|
24
29
|
}>;
|
|
25
30
|
execute(client: PosterlyClient, input: {
|
|
26
31
|
account_id: number;
|
|
@@ -28,5 +33,7 @@ export declare const getPostAnalyticsTool: {
|
|
|
28
33
|
to?: string;
|
|
29
34
|
limit?: number;
|
|
30
35
|
offset?: number;
|
|
36
|
+
presentation?: AnalyticsPresentation;
|
|
31
37
|
}): Promise<string>;
|
|
32
38
|
};
|
|
39
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
|
|
2
3
|
export const getPostAnalyticsTool = {
|
|
3
4
|
name: 'get_post_analytics',
|
|
4
|
-
description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first.',
|
|
5
|
+
description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first. presentation controls output: compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
|
|
5
6
|
inputSchema: z.object({
|
|
6
7
|
account_id: z
|
|
7
8
|
.number()
|
|
@@ -21,61 +22,139 @@ export const getPostAnalyticsTool = {
|
|
|
21
22
|
.optional()
|
|
22
23
|
.describe('Number of posts to return (default 50, max 200)'),
|
|
23
24
|
offset: z.number().min(0).optional().describe('Pagination offset'),
|
|
25
|
+
presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
|
|
24
26
|
}),
|
|
25
27
|
async execute(client, input) {
|
|
26
28
|
const result = await client.getPostAnalytics(input);
|
|
27
|
-
|
|
28
|
-
if (posts.length === 0) {
|
|
29
|
-
return `No analytics found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`;
|
|
30
|
-
}
|
|
31
|
-
const lines = [
|
|
32
|
-
`Post analytics for @${account.username} (${account.platform})`,
|
|
33
|
-
`Range: ${range.from} → ${range.to} • Showing ${posts.length} of ${total}`,
|
|
34
|
-
'',
|
|
35
|
-
];
|
|
36
|
-
for (const p of posts) {
|
|
37
|
-
const postedAt = p.posted_at
|
|
38
|
-
? new Date(p.posted_at).toLocaleString()
|
|
39
|
-
: 'unknown date';
|
|
40
|
-
const caption = p.caption_snippet
|
|
41
|
-
? p.caption_snippet.length > 60
|
|
42
|
-
? `${p.caption_snippet.slice(0, 60)}…`
|
|
43
|
-
: p.caption_snippet
|
|
44
|
-
: '(no caption)';
|
|
45
|
-
const metrics = [
|
|
46
|
-
`${p.likes} likes`,
|
|
47
|
-
`${p.comments} comments`,
|
|
48
|
-
`${p.reach.toLocaleString()} reach`,
|
|
49
|
-
];
|
|
50
|
-
if (p.impressions)
|
|
51
|
-
metrics.push(`${p.impressions.toLocaleString()} impressions`);
|
|
52
|
-
if (p.saved)
|
|
53
|
-
metrics.push(`${p.saved} saved`);
|
|
54
|
-
if (p.shares)
|
|
55
|
-
metrics.push(`${p.shares} shares`);
|
|
56
|
-
if (p.plays)
|
|
57
|
-
metrics.push(`${p.plays.toLocaleString()} plays`);
|
|
58
|
-
if (account.platform === 'youtube' && p.total_watch_time_ms) {
|
|
59
|
-
metrics.push(`${Math.round(p.total_watch_time_ms / 60000).toLocaleString()} watch minutes`);
|
|
60
|
-
}
|
|
61
|
-
if (account.platform === 'youtube' && p.avg_watch_time_ms) {
|
|
62
|
-
metrics.push(`${Math.round(p.avg_watch_time_ms / 1000)}s avg view`);
|
|
63
|
-
}
|
|
64
|
-
if (p.url_link_clicks) {
|
|
65
|
-
metrics.push(account.platform === 'pinterest'
|
|
66
|
-
? `${p.url_link_clicks.toLocaleString()} outbound clicks`
|
|
67
|
-
: `${p.url_link_clicks.toLocaleString()} link clicks`);
|
|
68
|
-
}
|
|
69
|
-
if (p.user_profile_clicks) {
|
|
70
|
-
metrics.push(account.platform === 'pinterest'
|
|
71
|
-
? `${p.user_profile_clicks.toLocaleString()} pin clicks`
|
|
72
|
-
: `${p.user_profile_clicks.toLocaleString()} profile clicks`);
|
|
73
|
-
}
|
|
74
|
-
lines.push(`• [${postedAt}] ${caption}`);
|
|
75
|
-
lines.push(` ${metrics.join(' • ')}`);
|
|
76
|
-
if (p.permalink)
|
|
77
|
-
lines.push(` ${p.permalink}`);
|
|
78
|
-
}
|
|
79
|
-
return lines.join('\n');
|
|
29
|
+
return formatPostAnalytics(result, input.presentation || 'compact');
|
|
80
30
|
},
|
|
81
31
|
};
|
|
32
|
+
function formatPostAnalytics(result, presentation) {
|
|
33
|
+
const { account, range, posts, total } = result;
|
|
34
|
+
if (posts.length === 0) {
|
|
35
|
+
return `No analytics found for ${accountLabel(account.username)} (${account.platform}) between ${range.from} and ${range.to}.`;
|
|
36
|
+
}
|
|
37
|
+
const totals = posts.reduce((acc, post) => {
|
|
38
|
+
acc.likes += post.likes || 0;
|
|
39
|
+
acc.comments += post.comments || 0;
|
|
40
|
+
acc.reach += post.reach || 0;
|
|
41
|
+
acc.impressions += post.impressions || 0;
|
|
42
|
+
acc.shares += post.shares || 0;
|
|
43
|
+
acc.saves += post.saved || 0;
|
|
44
|
+
return acc;
|
|
45
|
+
}, { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, saves: 0 });
|
|
46
|
+
if (presentation === 'json') {
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
type: 'post_analytics',
|
|
49
|
+
account,
|
|
50
|
+
range,
|
|
51
|
+
returned: {
|
|
52
|
+
count: posts.length,
|
|
53
|
+
total,
|
|
54
|
+
},
|
|
55
|
+
totals,
|
|
56
|
+
posts,
|
|
57
|
+
}, null, 2);
|
|
58
|
+
}
|
|
59
|
+
if (presentation === 'table') {
|
|
60
|
+
return [
|
|
61
|
+
`**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
62
|
+
`_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
|
|
63
|
+
'',
|
|
64
|
+
'**Returned totals**',
|
|
65
|
+
markdownTable(['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'], [[
|
|
66
|
+
formatNumber(totals.likes),
|
|
67
|
+
formatNumber(totals.comments),
|
|
68
|
+
formatNumber(totals.reach),
|
|
69
|
+
formatNumber(totals.impressions),
|
|
70
|
+
formatNumber(totals.shares),
|
|
71
|
+
formatNumber(totals.saves),
|
|
72
|
+
]]),
|
|
73
|
+
'',
|
|
74
|
+
'**Posts**',
|
|
75
|
+
markdownTable(['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'], posts.map((post) => [
|
|
76
|
+
dateTime(post.posted_at),
|
|
77
|
+
post.id ? String(post.id) : post.platform_media_id,
|
|
78
|
+
compact(post.caption_snippet, 70) || '(no caption)',
|
|
79
|
+
formatNumber(post.reach),
|
|
80
|
+
formatNumber(post.likes),
|
|
81
|
+
formatNumber(post.comments),
|
|
82
|
+
postExtras(account.platform, post),
|
|
83
|
+
])),
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
const lines = [
|
|
87
|
+
`**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
88
|
+
`_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
|
|
89
|
+
'',
|
|
90
|
+
'**Returned totals**',
|
|
91
|
+
`- **Likes:** ${formatNumber(totals.likes)}`,
|
|
92
|
+
`- **Comments:** ${formatNumber(totals.comments)}`,
|
|
93
|
+
`- **Reach:** ${formatNumber(totals.reach)}`,
|
|
94
|
+
`- **Impressions:** ${formatNumber(totals.impressions)}`,
|
|
95
|
+
`- **Shares:** ${formatNumber(totals.shares)}`,
|
|
96
|
+
`- **Saves:** ${formatNumber(totals.saves)}`,
|
|
97
|
+
'',
|
|
98
|
+
'**Posts**',
|
|
99
|
+
];
|
|
100
|
+
posts.forEach((post, index) => {
|
|
101
|
+
lines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
|
|
102
|
+
lines.push(` Posted: ${dateTime(post.posted_at)}`);
|
|
103
|
+
lines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
|
|
104
|
+
lines.push(` Reach: ${formatNumber(post.reach)}`);
|
|
105
|
+
lines.push(` Likes: ${formatNumber(post.likes)}`);
|
|
106
|
+
lines.push(` Comments: ${formatNumber(post.comments)}`);
|
|
107
|
+
lines.push(` Extras: ${postExtras(account.platform, post)}`);
|
|
108
|
+
});
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
function postExtras(platform, post) {
|
|
112
|
+
const metrics = [];
|
|
113
|
+
if (post.impressions)
|
|
114
|
+
metrics.push(`${formatNumber(post.impressions)} impressions`);
|
|
115
|
+
if (post.saved)
|
|
116
|
+
metrics.push(`${formatNumber(post.saved)} saved`);
|
|
117
|
+
if (post.shares)
|
|
118
|
+
metrics.push(`${formatNumber(post.shares)} shares`);
|
|
119
|
+
if (post.plays)
|
|
120
|
+
metrics.push(`${formatNumber(post.plays)} plays`);
|
|
121
|
+
if (platform === 'youtube' && post.total_watch_time_ms)
|
|
122
|
+
metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
|
|
123
|
+
if (platform === 'youtube' && post.avg_watch_time_ms)
|
|
124
|
+
metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
|
|
125
|
+
if (post.url_link_clicks)
|
|
126
|
+
metrics.push(platform === 'pinterest' ? `${formatNumber(post.url_link_clicks)} outbound clicks` : `${formatNumber(post.url_link_clicks)} link clicks`);
|
|
127
|
+
if (post.user_profile_clicks)
|
|
128
|
+
metrics.push(platform === 'pinterest' ? `${formatNumber(post.user_profile_clicks)} pin clicks` : `${formatNumber(post.user_profile_clicks)} profile clicks`);
|
|
129
|
+
if (post.permalink)
|
|
130
|
+
metrics.push(post.permalink);
|
|
131
|
+
return metrics.join(' · ') || 'n/a';
|
|
132
|
+
}
|
|
133
|
+
function accountLabel(username) {
|
|
134
|
+
if (!username)
|
|
135
|
+
return 'account';
|
|
136
|
+
return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
|
|
137
|
+
}
|
|
138
|
+
function dateTime(value) {
|
|
139
|
+
if (!value)
|
|
140
|
+
return 'n/a';
|
|
141
|
+
const date = new Date(value);
|
|
142
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
143
|
+
}
|
|
144
|
+
function compact(value, maxLength) {
|
|
145
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
146
|
+
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
|
|
147
|
+
}
|
|
148
|
+
function formatNumber(value) {
|
|
149
|
+
return value == null ? 'n/a' : value.toLocaleString();
|
|
150
|
+
}
|
|
151
|
+
function markdownTable(headers, rows) {
|
|
152
|
+
return [
|
|
153
|
+
`| ${headers.map(escapeCell).join(' | ')} |`,
|
|
154
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
155
|
+
...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
|
|
156
|
+
].join('\n');
|
|
157
|
+
}
|
|
158
|
+
function escapeCell(value) {
|
|
159
|
+
return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
160
|
+
}
|
package/package.json
CHANGED
package/src/lib/api-client.ts
CHANGED
|
@@ -152,7 +152,7 @@ export interface AccountAnalyticsSummary {
|
|
|
152
152
|
total_watch_minutes?: number | null;
|
|
153
153
|
total_likes?: number | null;
|
|
154
154
|
platform_metrics?: Record<string, number | null | undefined>;
|
|
155
|
-
display_metrics?: Array<{ label: string; value: number | null | undefined }>;
|
|
155
|
+
display_metrics?: Array<{ key?: string; label: string; value: number | null | undefined; unit?: string }>;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
export interface AccountAnalyticsSnapshot {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { AccountAnalyticsSummary, PosterlyClient } from '../lib/api-client.js';
|
|
2
|
+
import type { AccountAnalyticsResponse, AccountAnalyticsSummary, PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
|
|
5
|
+
type AnalyticsPresentation = z.infer<typeof presentationSchema>;
|
|
6
|
+
type MetricRow = [string, string];
|
|
3
7
|
|
|
4
8
|
export const getAccountAnalyticsTool = {
|
|
5
9
|
name: 'get_account_analytics',
|
|
6
10
|
description:
|
|
7
|
-
'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels
|
|
11
|
+
'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels and supports presentation: compact, table, or json.',
|
|
8
12
|
inputSchema: z.object({
|
|
9
13
|
account_id: z
|
|
10
14
|
.number()
|
|
@@ -17,92 +21,151 @@ export const getAccountAnalyticsTool = {
|
|
|
17
21
|
.string()
|
|
18
22
|
.optional()
|
|
19
23
|
.describe('End date (ISO date). Defaults to today.'),
|
|
24
|
+
presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
|
|
20
25
|
}),
|
|
21
26
|
|
|
22
27
|
async execute(
|
|
23
28
|
client: PosterlyClient,
|
|
24
|
-
input: { account_id: number; from?: string; to?: string }
|
|
29
|
+
input: { account_id: number; from?: string; to?: string; presentation?: AnalyticsPresentation }
|
|
25
30
|
) {
|
|
26
31
|
const result = await client.getAccountAnalytics(input);
|
|
27
|
-
|
|
32
|
+
return formatAccountAnalytics(result, input.presentation || 'compact');
|
|
33
|
+
},
|
|
34
|
+
};
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
function formatAccountAnalytics(result: AccountAnalyticsResponse, presentation: Exclude<AnalyticsPresentation, undefined>): string {
|
|
37
|
+
const { account, range, summary, snapshots } = result;
|
|
38
|
+
const rows = accountMetricRows(summary, account.platform);
|
|
39
|
+
const insights = analyticsInsights(summary, account.platform);
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
if (presentation === 'json') {
|
|
42
|
+
return JSON.stringify({
|
|
43
|
+
type: 'account_analytics',
|
|
44
|
+
account,
|
|
45
|
+
range,
|
|
46
|
+
snapshot_count: snapshots.length,
|
|
47
|
+
metrics: rows.map(([label, value]) => ({ label, value })),
|
|
48
|
+
insights,
|
|
49
|
+
summary,
|
|
50
|
+
snapshots,
|
|
51
|
+
}, null, 2);
|
|
52
|
+
}
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
};
|
|
54
|
+
if (presentation === 'table') {
|
|
55
|
+
return [
|
|
56
|
+
`**📊 Analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
57
|
+
`_${account.platform} · ${range.from} → ${range.to}_`,
|
|
58
|
+
'',
|
|
59
|
+
'**Account**',
|
|
60
|
+
markdownTable(['Field', 'Value'], [
|
|
61
|
+
['Account ID', String(account.id)],
|
|
62
|
+
['Daily snapshots', formatNumber(snapshots.length)],
|
|
63
|
+
]),
|
|
64
|
+
'',
|
|
65
|
+
'**Metrics**',
|
|
66
|
+
markdownTable(['Metric', 'Value'], rows),
|
|
67
|
+
'',
|
|
68
|
+
'**Quick read**',
|
|
69
|
+
insights.map((insight) => `- ${insight}`).join('\n'),
|
|
70
|
+
].join('\n');
|
|
71
|
+
}
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
return [
|
|
74
|
+
`**📊 Analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
75
|
+
`_${account.platform} · ${range.from} → ${range.to}_`,
|
|
76
|
+
'',
|
|
77
|
+
'**Account**',
|
|
78
|
+
`- **Account ID:** ${account.id}`,
|
|
79
|
+
`- **Daily snapshots:** ${formatNumber(snapshots.length)}`,
|
|
80
|
+
'',
|
|
81
|
+
'**Key metrics**',
|
|
82
|
+
rows.map(([label, value]) => `- **${label}:** ${value}`).join('\n'),
|
|
83
|
+
'',
|
|
84
|
+
'**Quick read**',
|
|
85
|
+
insights.map((insight) => `- ${insight}`).join('\n'),
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
61
88
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// from internal storage field names.
|
|
66
|
-
const apiRows = (summary.display_metrics || [])
|
|
67
|
-
.filter((metric) => metric?.label)
|
|
68
|
-
.map((metric) => [metric.label, metric.value] as MetricRow);
|
|
69
|
-
if (apiRows.length > 0) return apiRows;
|
|
89
|
+
function accountMetricRows(summary: AccountAnalyticsSummary, platform: string): MetricRow[] {
|
|
90
|
+
const nativeRows = nativeMetricRows(summary);
|
|
91
|
+
if (nativeRows.length > 0) return nativeRows;
|
|
70
92
|
|
|
71
93
|
if (platform === 'google_business') {
|
|
72
94
|
const metrics = summary.platform_metrics || {};
|
|
73
95
|
return [
|
|
74
|
-
['Profile Views', metrics.profile_views ?? summary.total_views],
|
|
75
|
-
['Search Views', metrics.search_views ?? summary.total_reach],
|
|
76
|
-
['Maps Views', metrics.maps_views ?? summary.total_profile_views],
|
|
77
|
-
['Customer Actions', metrics.customer_actions ?? summary.total_accounts_engaged],
|
|
78
|
-
['Posts', metrics.posts ?? summary.current_media_count],
|
|
96
|
+
['Profile Views', formatNumber(metrics.profile_views ?? summary.total_views)],
|
|
97
|
+
['Search Views', formatNumber(metrics.search_views ?? summary.total_reach)],
|
|
98
|
+
['Maps Views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
|
|
99
|
+
['Customer Actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
|
|
100
|
+
['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
|
|
79
101
|
];
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
|
|
104
|
+
const rows = genericMetricRows(summary);
|
|
105
|
+
if (platform === 'pinterest') rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
|
|
106
|
+
if (platform === 'youtube') {
|
|
107
|
+
rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
|
|
108
|
+
rows.push(['Likes', formatNumber(summary.total_likes)]);
|
|
109
|
+
}
|
|
110
|
+
return rows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function nativeMetricRows(summary: AccountAnalyticsSummary): MetricRow[] {
|
|
114
|
+
return (summary.display_metrics || [])
|
|
115
|
+
.filter((metric) => metric?.label)
|
|
116
|
+
.map((metric) => [metric.label, formatNumber(metric.value)]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function genericMetricRows(summary: AccountAnalyticsSummary): MetricRow[] {
|
|
120
|
+
return [
|
|
121
|
+
['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
|
|
122
|
+
['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
|
|
123
|
+
['Total reach', formatNumber(summary.total_reach)],
|
|
124
|
+
['Total views', formatNumber(summary.total_views)],
|
|
125
|
+
['Profile views', formatNumber(summary.total_profile_views)],
|
|
126
|
+
['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
|
|
127
|
+
['Engagement rate by reach', formatPercent(summary.engagement_rate)],
|
|
128
|
+
['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function analyticsInsights(summary: AccountAnalyticsSummary, platform: string): string[] {
|
|
133
|
+
const insights: string[] = [];
|
|
134
|
+
if (platform === 'google_business' && (summary.display_metrics?.length || 0) > 0) {
|
|
135
|
+
insights.push('Google Business metrics are shown with dashboard-native labels.');
|
|
136
|
+
}
|
|
137
|
+
if ((summary.followers_change || 0) > 0) insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
|
|
138
|
+
if ((summary.followers_change || 0) < 0) insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
|
|
139
|
+
if (summary.engagement_rate != null && platform !== 'google_business') insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
|
|
140
|
+
if (platform === 'youtube' && summary.total_watch_minutes != null) insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
|
|
141
|
+
return insights.length ? insights : ['No standout movement detected in the returned period.'];
|
|
83
142
|
}
|
|
84
143
|
|
|
85
144
|
function formatDelta(n: number): string {
|
|
86
145
|
return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
|
|
87
146
|
}
|
|
88
147
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
`• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`,
|
|
92
|
-
`• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`
|
|
93
|
-
);
|
|
94
|
-
pushMetric(lines, 'Total reach', summary.total_reach);
|
|
95
|
-
pushMetric(lines, 'Total views', summary.total_views);
|
|
96
|
-
pushMetric(lines, 'Profile views', summary.total_profile_views);
|
|
97
|
-
pushMetric(lines, 'Total accounts engaged', summary.total_accounts_engaged);
|
|
98
|
-
lines.push(
|
|
99
|
-
`• Engagement rate (by reach): ${summary.engagement_rate}%`,
|
|
100
|
-
`• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`
|
|
101
|
-
);
|
|
148
|
+
function formatNumber(value: number | null | undefined): string {
|
|
149
|
+
return value == null ? 'n/a' : value.toLocaleString();
|
|
102
150
|
}
|
|
103
151
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
152
|
+
function formatPercent(value: number | null | undefined): string {
|
|
153
|
+
return value == null ? 'n/a' : `${formatNumber(value)}%`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function accountLabel(username: string): string {
|
|
157
|
+
if (!username) return 'account';
|
|
158
|
+
return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function markdownTable(headers: string[], rows: MetricRow[]): string {
|
|
162
|
+
return [
|
|
163
|
+
`| ${headers.map(escapeCell).join(' | ')} |`,
|
|
164
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
165
|
+
...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
|
|
166
|
+
].join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function escapeCell(value: string): string {
|
|
170
|
+
return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
108
171
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { PosterlyClient } from '../lib/api-client.js';
|
|
2
|
+
import type { PostAnalyticsResponse, PostAnalyticsRow, PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
|
|
5
|
+
type AnalyticsPresentation = z.infer<typeof presentationSchema>;
|
|
3
6
|
|
|
4
7
|
export const getPostAnalyticsTool = {
|
|
5
8
|
name: 'get_post_analytics',
|
|
6
9
|
description:
|
|
7
|
-
'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first.',
|
|
10
|
+
'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first. presentation controls output: compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
|
|
8
11
|
inputSchema: z.object({
|
|
9
12
|
account_id: z
|
|
10
13
|
.number()
|
|
@@ -24,6 +27,7 @@ export const getPostAnalyticsTool = {
|
|
|
24
27
|
.optional()
|
|
25
28
|
.describe('Number of posts to return (default 50, max 200)'),
|
|
26
29
|
offset: z.number().min(0).optional().describe('Pagination offset'),
|
|
30
|
+
presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
|
|
27
31
|
}),
|
|
28
32
|
|
|
29
33
|
async execute(
|
|
@@ -34,65 +38,143 @@ export const getPostAnalyticsTool = {
|
|
|
34
38
|
to?: string;
|
|
35
39
|
limit?: number;
|
|
36
40
|
offset?: number;
|
|
41
|
+
presentation?: AnalyticsPresentation;
|
|
37
42
|
}
|
|
38
43
|
) {
|
|
39
44
|
const result = await client.getPostAnalytics(input);
|
|
40
|
-
|
|
45
|
+
return formatPostAnalytics(result, input.presentation || 'compact');
|
|
46
|
+
},
|
|
47
|
+
};
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
function formatPostAnalytics(result: PostAnalyticsResponse, presentation: Exclude<AnalyticsPresentation, undefined>): string {
|
|
50
|
+
const { account, range, posts, total } = result;
|
|
51
|
+
|
|
52
|
+
if (posts.length === 0) {
|
|
53
|
+
return `No analytics found for ${accountLabel(account.username)} (${account.platform}) between ${range.from} and ${range.to}.`;
|
|
54
|
+
}
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
const totals = posts.reduce((acc, post) => {
|
|
57
|
+
acc.likes += post.likes || 0;
|
|
58
|
+
acc.comments += post.comments || 0;
|
|
59
|
+
acc.reach += post.reach || 0;
|
|
60
|
+
acc.impressions += post.impressions || 0;
|
|
61
|
+
acc.shares += post.shares || 0;
|
|
62
|
+
acc.saves += post.saved || 0;
|
|
63
|
+
return acc;
|
|
64
|
+
}, { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, saves: 0 });
|
|
65
|
+
|
|
66
|
+
if (presentation === 'json') {
|
|
67
|
+
return JSON.stringify({
|
|
68
|
+
type: 'post_analytics',
|
|
69
|
+
account,
|
|
70
|
+
range,
|
|
71
|
+
returned: {
|
|
72
|
+
count: posts.length,
|
|
73
|
+
total,
|
|
74
|
+
},
|
|
75
|
+
totals,
|
|
76
|
+
posts,
|
|
77
|
+
}, null, 2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (presentation === 'table') {
|
|
81
|
+
return [
|
|
82
|
+
`**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
83
|
+
`_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
|
|
49
84
|
'',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
if (account.platform === 'youtube' && p.avg_watch_time_ms) {
|
|
74
|
-
metrics.push(`${Math.round(p.avg_watch_time_ms / 1000)}s avg view`);
|
|
75
|
-
}
|
|
76
|
-
if (p.url_link_clicks) {
|
|
77
|
-
metrics.push(
|
|
78
|
-
account.platform === 'pinterest'
|
|
79
|
-
? `${p.url_link_clicks.toLocaleString()} outbound clicks`
|
|
80
|
-
: `${p.url_link_clicks.toLocaleString()} link clicks`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (p.user_profile_clicks) {
|
|
84
|
-
metrics.push(
|
|
85
|
-
account.platform === 'pinterest'
|
|
86
|
-
? `${p.user_profile_clicks.toLocaleString()} pin clicks`
|
|
87
|
-
: `${p.user_profile_clicks.toLocaleString()} profile clicks`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
lines.push(`• [${postedAt}] ${caption}`);
|
|
92
|
-
lines.push(` ${metrics.join(' • ')}`);
|
|
93
|
-
if (p.permalink) lines.push(` ${p.permalink}`);
|
|
94
|
-
}
|
|
85
|
+
'**Returned totals**',
|
|
86
|
+
markdownTable(['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'], [[
|
|
87
|
+
formatNumber(totals.likes),
|
|
88
|
+
formatNumber(totals.comments),
|
|
89
|
+
formatNumber(totals.reach),
|
|
90
|
+
formatNumber(totals.impressions),
|
|
91
|
+
formatNumber(totals.shares),
|
|
92
|
+
formatNumber(totals.saves),
|
|
93
|
+
]]),
|
|
94
|
+
'',
|
|
95
|
+
'**Posts**',
|
|
96
|
+
markdownTable(['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'], posts.map((post) => [
|
|
97
|
+
dateTime(post.posted_at),
|
|
98
|
+
post.id ? String(post.id) : post.platform_media_id,
|
|
99
|
+
compact(post.caption_snippet, 70) || '(no caption)',
|
|
100
|
+
formatNumber(post.reach),
|
|
101
|
+
formatNumber(post.likes),
|
|
102
|
+
formatNumber(post.comments),
|
|
103
|
+
postExtras(account.platform, post),
|
|
104
|
+
])),
|
|
105
|
+
].join('\n');
|
|
106
|
+
}
|
|
95
107
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
108
|
+
const lines: string[] = [
|
|
109
|
+
`**📈 Post analytics for ${accountLabel(account.username)} (${account.id})**`,
|
|
110
|
+
`_${account.platform} · ${range.from} → ${range.to} · showing ${posts.length} of ${total}_`,
|
|
111
|
+
'',
|
|
112
|
+
'**Returned totals**',
|
|
113
|
+
`- **Likes:** ${formatNumber(totals.likes)}`,
|
|
114
|
+
`- **Comments:** ${formatNumber(totals.comments)}`,
|
|
115
|
+
`- **Reach:** ${formatNumber(totals.reach)}`,
|
|
116
|
+
`- **Impressions:** ${formatNumber(totals.impressions)}`,
|
|
117
|
+
`- **Shares:** ${formatNumber(totals.shares)}`,
|
|
118
|
+
`- **Saves:** ${formatNumber(totals.saves)}`,
|
|
119
|
+
'',
|
|
120
|
+
'**Posts**',
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
posts.forEach((post, index) => {
|
|
124
|
+
lines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
|
|
125
|
+
lines.push(` Posted: ${dateTime(post.posted_at)}`);
|
|
126
|
+
lines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
|
|
127
|
+
lines.push(` Reach: ${formatNumber(post.reach)}`);
|
|
128
|
+
lines.push(` Likes: ${formatNumber(post.likes)}`);
|
|
129
|
+
lines.push(` Comments: ${formatNumber(post.comments)}`);
|
|
130
|
+
lines.push(` Extras: ${postExtras(account.platform, post)}`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function postExtras(platform: string, post: PostAnalyticsRow): string {
|
|
137
|
+
const metrics: string[] = [];
|
|
138
|
+
if (post.impressions) metrics.push(`${formatNumber(post.impressions)} impressions`);
|
|
139
|
+
if (post.saved) metrics.push(`${formatNumber(post.saved)} saved`);
|
|
140
|
+
if (post.shares) metrics.push(`${formatNumber(post.shares)} shares`);
|
|
141
|
+
if (post.plays) metrics.push(`${formatNumber(post.plays)} plays`);
|
|
142
|
+
if (platform === 'youtube' && post.total_watch_time_ms) metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
|
|
143
|
+
if (platform === 'youtube' && post.avg_watch_time_ms) metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
|
|
144
|
+
if (post.url_link_clicks) metrics.push(platform === 'pinterest' ? `${formatNumber(post.url_link_clicks)} outbound clicks` : `${formatNumber(post.url_link_clicks)} link clicks`);
|
|
145
|
+
if (post.user_profile_clicks) metrics.push(platform === 'pinterest' ? `${formatNumber(post.user_profile_clicks)} pin clicks` : `${formatNumber(post.user_profile_clicks)} profile clicks`);
|
|
146
|
+
if (post.permalink) metrics.push(post.permalink);
|
|
147
|
+
return metrics.join(' · ') || 'n/a';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function accountLabel(username: string): string {
|
|
151
|
+
if (!username) return 'account';
|
|
152
|
+
return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function dateTime(value: string | null): string {
|
|
156
|
+
if (!value) return 'n/a';
|
|
157
|
+
const date = new Date(value);
|
|
158
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function compact(value: string | null, maxLength: number): string {
|
|
162
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
163
|
+
return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatNumber(value: number | null | undefined): string {
|
|
167
|
+
return value == null ? 'n/a' : value.toLocaleString();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function markdownTable(headers: string[], rows: string[][]): string {
|
|
171
|
+
return [
|
|
172
|
+
`| ${headers.map(escapeCell).join(' | ')} |`,
|
|
173
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
174
|
+
...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function escapeCell(value: string): string {
|
|
179
|
+
return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
180
|
+
}
|