Skip to content

Commit

Permalink
feat(contents): integrar Umami para views de conteúdo
Browse files Browse the repository at this point in the history
Integra o Umami Analytics para rastrear visualizações de conteúdo no site.

re filipedeschamps#1115
  • Loading branch information
mthmcalixto committed Nov 23, 2024
1 parent c747083 commit 06cfbde
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ EMAIL_HTTP_PORT=1080
EMAIL_USER=
EMAIL_PASSWORD=
UNDER_MAINTENANCE={"methodsAndPaths":["POST /api/v1/under-maintenance-test$"]}
UMAMI_KEY=
NEXT_PUBLIC_UMAMI_ENABLED=false
NEXT_PUBLIC_UMAMI_ID_WEBSITE=
51 changes: 51 additions & 0 deletions models/views-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
async function get(content) {
if (process.env.NEXT_PUBLIC_UMAMI_ENABLED !== 'true') {
return { metrics: [] };
}

if (!content || !content.created_at || !content.owner_username || !content.slug) {
return { metrics: [] };
}

const startAt = new Date(content.created_at).getTime();
const endAt = Date.now();
const url = `/${content.owner_username}/${content.slug}`;

const params = new URLSearchParams({
startAt: String(startAt),
endAt: String(endAt),
url,
});

try {
const res = await fetch(
`https://api.umami.is/v1/websites/${process.env.NEXT_PUBLIC_UMAMI_ID_WEBSITE}/stats?${params.toString()}`,
{
method: 'GET',
headers: {
'x-umami-api-key': process.env.UMAMI_KEY,
'Content-Type': 'application/json',
},
},
);

if (!res.ok) {
return { metrics: [] };
}

const data = await res.json();

const metrics = {
slug: url,
infos: data,
};

return { metrics };
} catch (error) {
return { metrics: [] };
}
}

export default Object.freeze({
get,
});
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@resvg/resvg-js": "2.6.2",
"@tabnews/forms": "0.2.2",
"@tabnews/ui": "0.3.1",
"@umami/node": "^0.4.0",
"@upstash/ratelimit": "1.2.1",
"@upstash/redis": "1.31.6",
"@vercel/analytics": "1.3.1",
Expand Down
4 changes: 3 additions & 1 deletion pages/_app.public.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RevalidateProvider } from 'next-swr';
import { SWRConfig } from 'swr';

import { ThemeProvider, Turnstile } from '@/TabNewsUI';
import { DefaultHead, UserProvider } from 'pages/interface';
import { DefaultHead, UserProvider, useUmamiTracking } from 'pages/interface';

async function SWRFetcher(resource, init) {
const response = await fetch(resource, init);
Expand All @@ -20,6 +20,8 @@ async function SWRFetcher(resource, init) {
const fallbackData = { body: null, headers: {} };

function MyApp({ Component, pageProps }) {
useUmamiTracking();

return (
<ThemeProvider>
<Turnstile />
Expand Down
55 changes: 55 additions & 0 deletions pages/api/v1/contents/[username]/[slug]/views/index.public.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import nextConnect from 'next-connect';

import { NotFoundError } from 'errors';
import cacheControl from 'models/cache-control';
import content from 'models/content';
import controller from 'models/controller';
import validator from 'models/validator';
import viewsContent from 'models/views-content';

export default nextConnect({
attachParams: true,
onNoMatch: controller.onNoMatchHandler,
onError: controller.onErrorHandler,
})
.use(controller.injectRequestMetadata)
.use(controller.logRequest)
.use(cacheControl.swrMaxAge(60))
.get(getValidationHandler, getHandler);

function getValidationHandler(request, response, next) {
const cleanValues = validator(request.query, {
username: 'required',
slug: 'required',
});

request.query = cleanValues;

next();
}

async function getHandler(request, response) {
const contentFound = await content.findOne({
where: {
owner_username: request.query.username,
slug: request.query.slug,
status: 'published',
},
});

if (!contentFound) {
throw new NotFoundError({
message: `Este conteúdo não está disponível.`,
action: 'Verifique se o "slug" está digitado corretamente ou considere o fato do conteúdo ter sido despublicado.',
stack: new Error().stack,
errorLocationCode: 'CONTROLLER:CONTENT:VIEWS:GET_HANDLER:SLUG_NOT_FOUND',
key: 'slug',
});
}

const getViews = await viewsContent.get(contentFound);

response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(getViews));
}
65 changes: 65 additions & 0 deletions pages/interface/hooks/useUmamiTracking/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import umami from '@umami/node';
import { useRouter } from 'next/router';
import { useEffect, useRef } from 'react';

const isValidPageForTracking = (routerPath, validPages) => {
return validPages.some((page) => routerPath.startsWith(page));
};

export default function useUmamiTracking() {
const router = useRouter();
const trackedPaths = useRef(new Set());

useEffect(() => {
const isUmamiEnabled = process.env.NEXT_PUBLIC_UMAMI_ENABLED === 'true';

const validPages = ['/login', '/cadastro'];

if (!isUmamiEnabled) return;

if (!umami.initCalled) {
umami.init({
websiteId: process.env.NEXT_PUBLIC_UMAMI_ID_WEBSITE,
hostUrl: 'https://cloud.umami.is',
});
umami.initCalled = true;
}

const trackPageView = (url) => {
if (!trackedPaths.current.has(url)) {
try {
umami.track({
url,
hostname: window.location.hostname,
language: window.navigator.language,
screen: `${window.screen.width}x${window.screen.height}`,
title: document.title,
});
trackedPaths.current.add(url);
} catch (error) {
console.error(`Error tracking page view for URL: ${url}`, error);
}
}
};

const currentPath = router.asPath;

if (isValidPageForTracking(currentPath, validPages)) {
trackPageView(currentPath);
}

const handleRouteChange = (url) => {
if (isValidPageForTracking(url, validPages)) {
trackPageView(url);
}
};

router.events.on('routeChangeComplete', handleRouteChange);

return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router]);

return null;
}
1 change: 1 addition & 0 deletions pages/interface/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { DefaultHead, default as Head } from './components/Head';
export { default as useCollapse } from './hooks/useCollapse';
export { default as useMediaQuery } from './hooks/useMediaQuery';
export { default as useUmamiTracking } from './hooks/useUmamiTracking';
export { UserProvider, default as useUser } from './hooks/useUser';
export { default as suggestEmail } from './utils/email-suggestion';
export { default as createErrorMessage } from './utils/error-message';
Expand Down

0 comments on commit 06cfbde

Please sign in to comment.