import Replay from '#models/replay' import env from '#start/env' import type { HttpContext } from '@adonisjs/core/http' import db from '@adonisjs/lucid/services/db' const SENTRY_TOKEN = env.get('SENTRY_TOKEN') const SENTRY_ORG = env.get('SENTRY_ORG') let recordsUpdated = 0 import redis from '@adonisjs/redis/services/main' import { DateTime } from 'luxon' const thirtyDaysAgo = DateTime.now().minus({ days: 30 }).toSQL() interface ApiResponse { data: T; // optionally, you can define `meta`, `errors`, etc. if your API returns them } interface SentryPagination { previous: string; hasPreviousResults: boolean; hasNextResults: boolean; next: string } export default class ReplaysController { public async search({ response}: HttpContext) { console.log('thir', thirtyDaysAgo) let results = await db.rawQuery(`SELECT "user" ->> 'display_name' AS user_email, SUM(duration) AS duration, COUNT(duration) AS sessions, AVG(duration) AS avg_duration FROM replays WHERE finished_at >= ? GROUP BY user_email ORDER BY SUM(duration) desc`, [thirtyDaysAgo] ) response.json(results) } public async list({ request, inertia }: HttpContext) { const page = request.input('page', 1) const perPage = 20 const cacheKey = `replays:page:${page}` let data = await redis.get(cacheKey) let paginated, meta, replays if (data) { ({ paginated, meta, replays } = JSON.parse(data)) } else { paginated = await Replay.query().paginate(page, perPage) paginated.baseUrl('/list') const json = paginated.toJSON() meta = { ...json.meta, links: buildPaginationLinks(json.meta) } replays = json.data await redis.set(cacheKey, JSON.stringify({ paginated, meta, replays }), 'EX', 60) } return inertia.render('Replays/Index', { data: { replays, meta } }) } async index({ request, response }: HttpContext) { const {statsPeriod, start, end} = request.qs() recordsUpdated = 0 let queryString: string = '?statsPeriod=24h'// Default in case none is provided if (statsPeriod) { queryString = `?statsPeriod=${statsPeriod}` } else if (start && end) { queryString = `?start=${start}&end=${end}` } const replays = await fetchBatch(`https://sentry.io/api/0/organizations/${SENTRY_ORG}/replays/${queryString}`) return response.json(replays) } } async function fetchBatch(url: string) { const options: RequestInit = { headers: { Authorization: `Bearer ${SENTRY_TOKEN}` } } const req = await fetch(url, options) if (!req.ok) { throw new Error(`Request failed with status ${req.status}`); } const resp = await req.json() as ApiResponse; const replays = resp.data; const headers = req.headers const cleanedData = replays.map(record => sanitizeInput(record, Replay.allowedFields)) let updated = await Replay.updateOrCreateMany('id', cleanedData ) recordsUpdated = recordsUpdated + updated.length const linkHeader = headers.get('link') if (!linkHeader) { return {error: 'link header missing from Sentry API response'} } const pagination: SentryPagination = parseSentryLinkHeader(linkHeader) if (pagination.hasNextResults == true) { console.log('fetching', pagination.next) await fetchBatch(pagination.next) } console.log('no more results') return {recordsUpdated} } function parseSentryLinkHeader(header:string): SentryPagination { const links = header.split(',').map(part => part.trim()) let result = {} as SentryPagination for (const link of links) { const match = link.match(/<([^>]+)>;\s*rel="([^"]+)";\s*results="([^"]+)";\s*cursor="([^"]+)"/) if (!match) continue const [, url, rel, results] = match if (rel === 'previous') { result.previous = url result.hasPreviousResults = results === 'true' } else if (rel === 'next') { result.next = url result.hasNextResults = results === 'true' } } return result } function sanitizeInput(data: Record, allowedFields: string[]) { return allowedFields.reduce((acc, key) => { if (key in data) acc[key] = data[key] return acc }, {} as Record) } function buildPaginationLinks(meta: { previousPageUrl: string, lastPage: number; currentPage: number; nextPageUrl: string}) { const links = [] // Previous links.push({ url: meta.previousPageUrl, label: '« Prev', active: false }) for (let page = 1; page <= meta.lastPage; page++) { links.push({ url: `/list?page=${page}`, label: page.toString(), active: page === meta.currentPage }) } // Next links.push({ url: meta.nextPageUrl, label: 'Next »', active: false }) return links }