feat: post analytics

This commit is contained in:
Nevo David 2026-01-21 15:11:29 +07:00
parent 99889d59e2
commit 5cca81e002
16 changed files with 865 additions and 25 deletions

View File

@ -3,11 +3,15 @@ import { Organization } from '@prisma/client';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { ApiTags } from '@nestjs/swagger';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
@ApiTags('Analytics')
@Controller('/analytics')
export class AnalyticsController {
constructor(private _integrationService: IntegrationService) {}
constructor(
private _integrationService: IntegrationService,
private _postsService: PostsService
) {}
@Get('/:integration')
async getIntegration(
@ -17,4 +21,13 @@ export class AnalyticsController {
) {
return this._integrationService.checkAnalytics(org, integration, date);
}
@Get('/post/:postId')
async getPostAnalytics(
@GetOrgFromRequest() org: Organization,
@Param('postId') postId: string,
@Query('date') date: string
) {
return this._postsService.checkPostAnalytics(org.id, postId, +date);
}
}

View File

@ -876,7 +876,7 @@ const CalendarItem: FC<{
>
<Preview />
</div>{' '}
{post.integration.providerIdentifier === 'x' && disableXAnalytics ? (
{((post.integration.providerIdentifier === 'x' && disableXAnalytics) || !post.releaseId) ? (
<></>
) : (
<div

View File

@ -1,36 +1,138 @@
import React, { FC, Fragment, useCallback } from 'react';
import { useModals } from '@gitroom/frontend/components/layout/new-modal';
import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { ChartSocial } from '@gitroom/frontend/components/analytics/chart-social';
import { Select } from '@gitroom/react/form/select';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
interface AnalyticsData {
label: string;
data: Array<{ total: number; date: string }>;
percentageChange: number;
average?: boolean;
}
export const StatisticsModal: FC<{
postId: string;
}> = (props) => {
const { postId } = props;
const modals = useModals();
const t = useT();
const fetch = useFetch();
const [dateRange, setDateRange] = useState(7);
const loadStatistics = useCallback(async () => {
return (await fetch(`/posts/${postId}/statistics`)).json();
}, [postId]);
const closeAll = useCallback(() => {
modals.closeAll();
}, []);
const { data, isLoading } = useSWR(
}, [postId, fetch]);
const loadPostAnalytics = useCallback(async () => {
return (await fetch(`/analytics/post/${postId}?date=${dateRange}`)).json();
}, [postId, dateRange, fetch]);
const { data: statisticsData, isLoading: isLoadingStatistics } = useSWR(
`/posts/${postId}/statistics`,
loadStatistics
);
const { data: analyticsData, isLoading: isLoadingAnalytics } = useSWR(
`/analytics/post/${postId}?date=${dateRange}`,
loadPostAnalytics,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
revalidateOnMount: true,
refreshWhenHidden: false,
refreshWhenOffline: false,
}
);
const dateOptions = useMemo(() => {
return [
{ key: 7, value: t('7_days', '7 Days') },
{ key: 30, value: t('30_days', '30 Days') },
{ key: 90, value: t('90_days', '90 Days') },
];
}, [t]);
const totals = useMemo(() => {
if (!analyticsData || !Array.isArray(analyticsData)) return [];
return analyticsData.map((p: AnalyticsData) => {
const value =
(p?.data?.reduce((acc: number, curr: any) => acc + Number(curr.total), 0) || 0) /
(p.average ? p.data.length : 1);
if (p.average) {
return value.toFixed(2) + '%';
}
return Math.round(value);
});
}, [analyticsData]);
const isLoading = isLoadingStatistics || isLoadingAnalytics;
return (
<div className="relative">
<div className="relative min-h-[200px]">
{isLoading ? (
<div>{t('loading', 'Loading')}</div>
<div className="flex items-center justify-center py-[40px]">
<LoadingComponent />
</div>
) : (
<>
{data.clicks.length === 0 ? (
'No Results'
) : (
<>
<div className="grid grid-cols-3 mt-[20px]">
<div className="flex flex-col gap-[24px]">
{/* Post Analytics Section */}
{analyticsData && Array.isArray(analyticsData) && analyticsData.length > 0 && (
<div className="flex flex-col gap-[14px]">
<div className="flex items-center justify-between">
<h3 className="text-[18px] font-[500]">
{t('post_analytics', 'Post Analytics')}
</h3>
<div className="max-w-[150px]">
<Select
label=""
name="date"
disableForm={true}
hideErrors={true}
value={dateRange}
onChange={(e) => setDateRange(+e.target.value)}
>
{dateOptions.map((option) => (
<option key={option.key} value={option.key}>
{option.value}
</option>
))}
</Select>
</div>
</div>
<div className="grid grid-cols-3 gap-[20px]">
{analyticsData.map((p: AnalyticsData, index: number) => (
<div key={`analytics-${index}`} className="flex">
<div className="flex-1 bg-newTableHeader rounded-[8px] py-[10px] px-[16px] gap-[10px] flex flex-col">
<div className="flex items-center gap-[14px]">
<div className="text-[20px]">{p.label}</div>
</div>
<div className="flex-1">
<div className="h-[156px] relative">
<ChartSocial data={p.data} key={`chart-${index}`} />
</div>
</div>
<div className="text-[50px] leading-[60px]">{totals[index]}</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Short Links Statistics Section */}
<div className="flex flex-col gap-[14px]">
<h3 className="text-[18px] font-[500]">
{t('short_links_statistics', 'Short Links Statistics')}
</h3>
{statisticsData?.clicks?.length === 0 ? (
<div className="text-gray-400">
{t('no_short_link_results', 'No short link results')}
</div>
) : (
<div className="grid grid-cols-3">
<div className="bg-forth p-[4px] rounded-tl-lg">
{t('short_link', 'Short Link')}
</div>
@ -40,7 +142,7 @@ export const StatisticsModal: FC<{
<div className="bg-forth p-[4px] rounded-tr-lg">
{t('clicks', 'Clicks')}
</div>
{data.clicks.map((p: any) => (
{statisticsData?.clicks?.map((p: any) => (
<Fragment key={p.short}>
<div className="p-[4px] py-[10px] bg-customColor6">
{p.short}
@ -54,9 +156,17 @@ export const StatisticsModal: FC<{
</Fragment>
))}
</div>
</>
)}
</>
)}
</div>
{/* No analytics available message */}
{(!analyticsData || !Array.isArray(analyticsData) || analyticsData.length === 0) &&
(!statisticsData?.clicks || statisticsData.clicks.length === 0) && (
<div className="text-center text-gray-400 py-[20px]">
{t('no_statistics_available', 'No statistics available for this post')}
</div>
)}
</div>
)}
</div>
);

View File

@ -168,8 +168,7 @@ export class PostsRepository {
content: true,
publishDate: true,
releaseURL: true,
submittedForOrganizationId: true,
submittedForOrderId: true,
releaseId: true,
state: true,
intervalInDays: true,
group: true,

View File

@ -30,6 +30,11 @@ import {
organizationId,
postId as postIdSearchParam,
} from '@gitroom/nestjs-libraries/temporal/temporal.search.attribute';
import { AnalyticsData } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { timer } from '@gitroom/helpers/utils/timer';
import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service';
type PostWithConditionals = Post & {
integration?: Integration;
@ -46,7 +51,8 @@ export class PostsService {
private _mediaService: MediaService,
private _shortLinkService: ShortLinkService,
private _openaiService: OpenaiService,
private _temporalService: TemporalService
private _temporalService: TemporalService,
private _refreshIntegrationService: RefreshIntegrationService
) {}
searchForMissingThreeHoursPosts() {
@ -57,6 +63,85 @@ export class PostsService {
return this._postRepository.updatePost(id, postId, releaseURL);
}
async checkPostAnalytics(
orgId: string,
postId: string,
date: number,
forceRefresh = false
): Promise<AnalyticsData[]> {
const post = await this._postRepository.getPostById(postId, orgId);
if (!post || !post.releaseId) {
return [];
}
const integrationProvider = this._integrationManager.getSocialIntegration(
post.integration.providerIdentifier
);
if (!integrationProvider.postAnalytics) {
return [];
}
const getIntegration = post.integration!;
if (
dayjs(getIntegration?.tokenExpiration).isBefore(dayjs()) ||
forceRefresh
) {
const data = await this._refreshIntegrationService.refresh(
getIntegration
);
if (!data) {
return [];
}
const { accessToken } = data;
if (accessToken) {
getIntegration.token = accessToken;
if (integrationProvider.refreshWait) {
await timer(10000);
}
} else {
await this._integrationService.disconnectChannel(orgId, getIntegration);
return [];
}
}
const getIntegrationData = await ioRedis.get(
`integration:${orgId}:${post.id}:${date}`
);
if (getIntegrationData) {
return JSON.parse(getIntegrationData);
}
try {
const loadAnalytics = await integrationProvider.postAnalytics(
getIntegration.internalId,
getIntegration.token,
post.releaseId,
date
);
await ioRedis.set(
`integration:${orgId}:${post.id}:${date}`,
JSON.stringify(loadAnalytics),
'EX',
!process.env.NODE_ENV || process.env.NODE_ENV === 'development'
? 1
: 3600
);
return loadAnalytics;
} catch (e) {
console.log(e);
if (e instanceof RefreshToken) {
return this.checkPostAnalytics(orgId, postId, date, true);
}
}
return [];
}
async getStatistics(orgId: string, id: string) {
const getPost = await this.getPostsRecursively(id, true, orgId, true);
const content = getPost.map((p) => p.content);

View File

@ -187,4 +187,14 @@ export class DribbbleProvider extends SocialAbstract implements SocialProvider {
): Promise<AnalyticsData[]> {
return Promise.resolve([]);
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
// Dribbble doesn't provide detailed post-level analytics via their API
return [];
}
}

View File

@ -462,4 +462,79 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider {
})) || []
);
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
const today = dayjs().format('YYYY-MM-DD');
try {
// Fetch post insights from Facebook Graph API
const { data } = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${postId}/insights?metric=post_impressions,post_impressions_unique,post_engaged_users,post_clicks,post_reactions_by_type_total&access_token=${accessToken}`
)
).json();
if (!data || data.length === 0) {
return [];
}
const result: AnalyticsData[] = [];
for (const metric of data) {
const value = metric.values?.[0]?.value;
if (value === undefined) continue;
let label = '';
let total = '';
switch (metric.name) {
case 'post_impressions':
label = 'Impressions';
total = String(value);
break;
case 'post_impressions_unique':
label = 'Reach';
total = String(value);
break;
case 'post_engaged_users':
label = 'Engaged Users';
total = String(value);
break;
case 'post_clicks':
label = 'Clicks';
total = String(value);
break;
case 'post_reactions_by_type_total':
// This returns an object with reaction types
if (typeof value === 'object') {
const totalReactions = Object.values(value as Record<string, number>).reduce(
(sum: number, v: number) => sum + v,
0
);
label = 'Reactions';
total = String(totalReactions);
}
break;
}
if (label) {
result.push({
label,
percentageChange: 0,
data: [{ total, date: today }],
});
}
}
return result;
} catch (err) {
console.error('Error fetching Facebook post analytics:', err);
return [];
}
}
}

View File

@ -560,4 +560,15 @@ export class GmbProvider extends SocialAbstract implements SocialProvider {
return [];
}
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
// Google My Business local posts don't have detailed individual post analytics
// The API focuses on location-level metrics rather than post-level metrics
return [];
}
}

View File

@ -741,4 +741,73 @@ export class InstagramProvider
)}&access_token=${accessToken}`
);
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number,
type = 'graph.facebook.com'
): Promise<AnalyticsData[]> {
const today = dayjs().format('YYYY-MM-DD');
try {
// Fetch media insights from Instagram Graph API
const { data } = await (
await this.fetch(
`https://${type}/v21.0/${postId}/insights?metric=impressions,reach,engagement,saved,likes,comments,shares&access_token=${accessToken}`
)
).json();
if (!data || data.length === 0) {
return [];
}
const result: AnalyticsData[] = [];
for (const metric of data) {
const value = metric.values?.[0]?.value;
if (value === undefined) continue;
let label = '';
switch (metric.name) {
case 'impressions':
label = 'Impressions';
break;
case 'reach':
label = 'Reach';
break;
case 'engagement':
label = 'Engagement';
break;
case 'saved':
label = 'Saves';
break;
case 'likes':
label = 'Likes';
break;
case 'comments':
label = 'Comments';
break;
case 'shares':
label = 'Shares';
break;
}
if (label) {
result.push({
label,
percentageChange: 0,
data: [{ total: String(value), date: today }],
});
}
}
return result;
} catch (err) {
console.error('Error fetching Instagram post analytics:', err);
return [];
}
}
}

View File

@ -193,4 +193,19 @@ export class InstagramStandaloneProvider
'graph.instagram.com'
);
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
) {
return instagramProvider.postAnalytics(
integrationId,
accessToken,
postId,
date,
'graph.instagram.com'
);
}
}

View File

@ -417,6 +417,132 @@ export class LinkedinPageProvider
}));
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
const endDate = dayjs().unix() * 1000;
const startDate = dayjs().subtract(date, 'days').unix() * 1000;
// Fetch share statistics for the specific post
const shareStatsUrl = `https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(
`urn:li:organization:${integrationId}`
)}&shares=List(${encodeURIComponent(postId)})&timeIntervals=(timeRange:(start:${startDate},end:${endDate}),timeGranularityType:DAY)`;
const { elements: shareElements }: { elements: PostShareStatElement[] } =
await (
await this.fetch(shareStatsUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
'LinkedIn-Version': '202511',
'X-Restli-Protocol-Version': '2.0.0',
},
})
).json();
// Also fetch social actions (likes, comments, shares) for the specific post
let socialActions: SocialActionsResponse | null = null;
try {
const socialActionsUrl = `https://api.linkedin.com/v2/socialActions/${encodeURIComponent(
postId
)}`;
socialActions = await (
await this.fetch(socialActionsUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
'LinkedIn-Version': '202511',
'X-Restli-Protocol-Version': '2.0.0',
},
})
).json();
} catch (e) {
// Social actions may not be available for all posts
}
// Process share statistics into time series data
const analytics = (shareElements || []).reduce(
(all, current) => {
if (typeof current?.totalShareStatistics !== 'undefined') {
const dateStr = dayjs(current.timeRange.start).format('YYYY-MM-DD');
all['Impressions'].push({
total: current.totalShareStatistics.impressionCount || 0,
date: dateStr,
});
all['Unique Impressions'].push({
total: current.totalShareStatistics.uniqueImpressionsCount || 0,
date: dateStr,
});
all['Clicks'].push({
total: current.totalShareStatistics.clickCount || 0,
date: dateStr,
});
all['Likes'].push({
total: current.totalShareStatistics.likeCount || 0,
date: dateStr,
});
all['Comments'].push({
total: current.totalShareStatistics.commentCount || 0,
date: dateStr,
});
all['Shares'].push({
total: current.totalShareStatistics.shareCount || 0,
date: dateStr,
});
all['Engagement'].push({
total: current.totalShareStatistics.engagement || 0,
date: dateStr,
});
}
return all;
},
{
Impressions: [] as { total: number; date: string }[],
'Unique Impressions': [] as { total: number; date: string }[],
Clicks: [] as { total: number; date: string }[],
Likes: [] as { total: number; date: string }[],
Comments: [] as { total: number; date: string }[],
Shares: [] as { total: number; date: string }[],
Engagement: [] as { total: number; date: string }[],
}
);
// If no time series data but we have social actions, create a single data point
if (
Object.values(analytics).every((arr) => arr.length === 0) &&
socialActions
) {
const today = dayjs().format('YYYY-MM-DD');
analytics['Likes'].push({
total: socialActions.likesSummary?.totalLikes || 0,
date: today,
});
analytics['Comments'].push({
total: socialActions.commentsSummary?.totalFirstLevelComments || 0,
date: today,
});
}
// Filter out empty analytics
const result = Object.entries(analytics)
.filter(([_, data]) => data.length > 0)
.map(([label, data]) => ({
label,
data,
percentageChange: 0,
}));
return result as any;
}
@Plug({
identifier: 'linkedin-page-autoRepostPost',
title: 'Auto Repost Posts',
@ -764,3 +890,30 @@ export interface TimeRange {
start: number;
end: number;
}
// Post analytics interfaces
export interface PostShareStatElement {
organizationalEntity: string;
share: string;
totalShareStatistics: {
uniqueImpressionsCount: number;
shareCount: number;
engagement: number;
clickCount: number;
likeCount: number;
impressionCount: number;
commentCount: number;
};
timeRange: TimeRange;
}
export interface SocialActionsResponse {
likesSummary?: {
totalLikes: number;
likedByCurrentUser: boolean;
};
commentsSummary?: {
totalFirstLevelComments: number;
commentsState: string;
};
}

View File

@ -358,4 +358,78 @@ export class PinterestProvider
]
);
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
const today = dayjs().format('YYYY-MM-DD');
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
try {
// Fetch pin analytics from Pinterest API
const response = await this.fetch(
`https://api.pinterest.com/v5/pins/${postId}/analytics?start_date=${since}&end_date=${today}&metric_types=IMPRESSION,PIN_CLICK,OUTBOUND_CLICK,SAVE`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
);
const data = await response.json();
if (!data || !data.all) {
return [];
}
const result: AnalyticsData[] = [];
const metrics = data.all;
if (metrics.lifetime_metrics) {
const lifetimeMetrics = metrics.lifetime_metrics;
if (lifetimeMetrics.IMPRESSION !== undefined) {
result.push({
label: 'Impressions',
percentageChange: 0,
data: [{ total: String(lifetimeMetrics.IMPRESSION), date: today }],
});
}
if (lifetimeMetrics.PIN_CLICK !== undefined) {
result.push({
label: 'Pin Clicks',
percentageChange: 0,
data: [{ total: String(lifetimeMetrics.PIN_CLICK), date: today }],
});
}
if (lifetimeMetrics.OUTBOUND_CLICK !== undefined) {
result.push({
label: 'Outbound Clicks',
percentageChange: 0,
data: [{ total: String(lifetimeMetrics.OUTBOUND_CLICK), date: today }],
});
}
if (lifetimeMetrics.SAVE !== undefined) {
result.push({
label: 'Saves',
percentageChange: 0,
data: [{ total: String(lifetimeMetrics.SAVE), date: today }],
});
}
}
return result;
} catch (err) {
console.error('Error fetching Pinterest post analytics:', err);
return [];
}
}
}

View File

@ -28,6 +28,12 @@ export interface IAuthenticator {
accessToken: string,
date: number
): Promise<AnalyticsData[]>;
postAnalytics?(
integrationId: string,
accessToken: string,
postId: string,
fromDate: number,
): Promise<AnalyticsData[]>;
changeNickname?(
id: string,
accessToken: string,
@ -46,6 +52,7 @@ export interface AnalyticsData {
percentageChange: number;
}
export type GenerateAuthUrlResponse = {
url: string;
codeVerifier: string;

View File

@ -523,6 +523,68 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider {
return false;
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
const today = dayjs().format('YYYY-MM-DD');
try {
// Fetch thread insights from Threads API
const { data } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${postId}/insights?metric=views,likes,replies,reposts,quotes&access_token=${accessToken}`
)
).json();
if (!data || data.length === 0) {
return [];
}
const result: AnalyticsData[] = [];
for (const metric of data) {
const value = metric.values?.[0]?.value ?? metric.total_value?.value;
if (value === undefined) continue;
let label = '';
switch (metric.name) {
case 'views':
label = 'Views';
break;
case 'likes':
label = 'Likes';
break;
case 'replies':
label = 'Replies';
break;
case 'reposts':
label = 'Reposts';
break;
case 'quotes':
label = 'Quotes';
break;
}
if (label) {
result.push({
label,
percentageChange: 0,
data: [{ total: String(value), date: today }],
});
}
}
return result;
} catch (err) {
console.error('Error fetching Threads post analytics:', err);
return [];
}
}
// override async mention(
// token: string,
// data: { query: string },

View File

@ -590,6 +590,96 @@ export class XProvider extends SocialAbstract implements SocialProvider {
return [];
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
if (process.env.DISABLE_X_ANALYTICS) {
return [];
}
const today = dayjs().format('YYYY-MM-DD');
const [accessTokenSplit, accessSecretSplit] = accessToken.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
try {
// Fetch the specific tweet with public metrics
const tweet = await client.v2.singleTweet(postId, {
'tweet.fields': ['public_metrics', 'created_at'],
});
if (!tweet?.data?.public_metrics) {
return [];
}
const metrics = tweet.data.public_metrics;
const result: AnalyticsData[] = [];
if (metrics.impression_count !== undefined) {
result.push({
label: 'Impressions',
percentageChange: 0,
data: [{ total: String(metrics.impression_count), date: today }],
});
}
if (metrics.like_count !== undefined) {
result.push({
label: 'Likes',
percentageChange: 0,
data: [{ total: String(metrics.like_count), date: today }],
});
}
if (metrics.retweet_count !== undefined) {
result.push({
label: 'Retweets',
percentageChange: 0,
data: [{ total: String(metrics.retweet_count), date: today }],
});
}
if (metrics.reply_count !== undefined) {
result.push({
label: 'Replies',
percentageChange: 0,
data: [{ total: String(metrics.reply_count), date: today }],
});
}
if (metrics.quote_count !== undefined) {
result.push({
label: 'Quotes',
percentageChange: 0,
data: [{ total: String(metrics.quote_count), date: today }],
});
}
if (metrics.bookmark_count !== undefined) {
result.push({
label: 'Bookmarks',
percentageChange: 0,
data: [{ total: String(metrics.bookmark_count), date: today }],
});
}
return result;
} catch (err) {
console.log('Error fetching X post analytics:', err);
}
return [];
}
override async mention(token: string, d: { query: string }) {
const [accessTokenSplit, accessSecretSplit] = token.split(':');
const client = new TwitterApi({

View File

@ -449,4 +449,71 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider {
return [];
}
}
async postAnalytics(
integrationId: string,
accessToken: string,
postId: string,
date: number
): Promise<AnalyticsData[]> {
const today = dayjs().format('YYYY-MM-DD');
try {
const { client, youtube } = clientAndYoutube();
client.setCredentials({ access_token: accessToken });
const youtubeClient = youtube(client);
// Fetch video statistics
const response = await youtubeClient.videos.list({
part: ['statistics', 'snippet'],
id: [postId],
});
const video = response.data.items?.[0];
if (!video || !video.statistics) {
return [];
}
const stats = video.statistics;
const result: AnalyticsData[] = [];
if (stats.viewCount !== undefined) {
result.push({
label: 'Views',
percentageChange: 0,
data: [{ total: String(stats.viewCount), date: today }],
});
}
if (stats.likeCount !== undefined) {
result.push({
label: 'Likes',
percentageChange: 0,
data: [{ total: String(stats.likeCount), date: today }],
});
}
if (stats.commentCount !== undefined) {
result.push({
label: 'Comments',
percentageChange: 0,
data: [{ total: String(stats.commentCount), date: today }],
});
}
if (stats.favoriteCount !== undefined) {
result.push({
label: 'Favorites',
percentageChange: 0,
data: [{ total: String(stats.favoriteCount), date: today }],
});
}
return result;
} catch (err) {
console.error('Error fetching YouTube post analytics:', err);
return [];
}
}
}