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 16, 2024
1 parent c747083 commit 68e42d6
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 0 deletions.
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=null
NEXT_PUBLIC_UMAMI_ENABLED=false
NEXT_PUBLIC_UMAMI_ID_WEBSITE=null
58 changes: 58 additions & 0 deletions models/views-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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),
timezone: 'America/Sao_Paulo',
unit: 'month',
url: url,
});

try {
const res = await fetch(
`https://api.umami.is/v1/websites/${process.env.NEXT_PUBLIC_UMAMI_ID_WEBSITE}/pageviews?${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();

if (Array.isArray(data.sessions)) {
const metrics = data.sessions.map((item) => ({
slug: url,
views: item.y,
}));

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

export default Object.freeze({
get,
});
13 changes: 13 additions & 0 deletions pages/_app.public.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Analytics } from '@vercel/analytics/react';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { RevalidateProvider } from 'next-swr';
import { SWRConfig } from 'swr';

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

function MyApp({ Component, pageProps }) {
const { asPath } = useRouter();
const isUmamiEnabled = process.env.NEXT_PUBLIC_UMAMI_ENABLED === 'true';

return (
<ThemeProvider>
<Turnstile />
Expand All @@ -39,6 +44,14 @@ function MyApp({ Component, pageProps }) {
return event;
}}
/>

{isUmamiEnabled && !['/', '/publicar'].includes(asPath) && (
<Script
async
src="https://cloud.umami.is/script.js"
data-website-id={`${process.env.NEXT_PUBLIC_UMAMI_ID_WEBSITE}`}
/>
)}
</UserProvider>
</ThemeProvider>
);
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));
}

0 comments on commit 68e42d6

Please sign in to comment.