Skip to content

Commit

Permalink
feat: latest version of packages + node 20 + fix strava + add logging…
Browse files Browse the repository at this point in the history
… + speed insights + analytics (#436)

Closes DG-148, DG-147

## What changed? Why?
- Upgrades to node 20
- Dependencies to latest
- Add speed insights from Vercel
- Add logging solution + debugging to Strava webhooks
- Add analytics from Vercel
- Tentative fix for Strava not syncing webhook data
  • Loading branch information
dgattey committed Jan 18, 2024
1 parent dffa092 commit 3dd4991
Show file tree
Hide file tree
Showing 38 changed files with 500 additions and 355 deletions.
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.17.1
20.11.0
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
save-exact=true
auto-install-peers=true
node-version=18.17.1
node-version=20.11.0
engine-strict=true
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.17.1
20.11.0
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Standard semver versioning is done via `semantic-release` and Conventional Commi
- **Minor**: bumped if "feat:" appears in the message
- **Patch**: bumped by default in all other cases ("chore:"/"fix:"/etc)

Test a dry run with `GITHUB_TOKEN=* pnpm turbo release -- --dry-run --branches={branch here}` after filling in the token.

### History

The site was originally Wordpress for way too many years! I used a custom theme + had a bunch of optimizations starting way back in ~2011.
Expand Down
4 changes: 3 additions & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { withLogtail } = require('@logtail/next');

require('dotenv-mono').config();

/**
Expand Down Expand Up @@ -36,4 +38,4 @@ const nextConfig = {
},
};

module.exports = withNextBundleAnalyzer(nextConfig);
module.exports = withNextBundleAnalyzer(withLogtail(nextConfig));
9 changes: 6 additions & 3 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
"@emotion/styled": "11.11.0",
"@fortawesome/free-brands-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@mui/material": "5.15.2",
"@logtail/next": "0.1.4",
"@mui/material": "5.15.5",
"@next/bundle-analyzer": "14.0.4",
"@vercel/analytics": "1.1.1",
"@vercel/speed-insights": "1.0.4",
"api": "workspace:*",
"dotenv-mono": "1.3.13",
"lucide-react": "0.303.0",
"lucide-react": "0.312.0",
"maps": "workspace:*",
"next": "14.0.4",
"og": "workspace:*",
Expand All @@ -37,7 +40,7 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "14.0.4",
"@types/react": "18.2.46",
"@types/react": "18.2.48",
"eslint-config-dg": "workspace:*",
"tsconfig": "workspace:*",
"typescript": "5.3.3"
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import { GlobalStyleProvider } from 'ui/theme/GlobalStyleProvider';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/react';

export type PageProps = Record<string, unknown>;

Expand All @@ -25,7 +27,11 @@ type AppPropsWithLayout = AppProps<PageProps> & {
function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<GlobalStyleProvider>{getLayout(<Component {...pageProps} />, pageProps)}</GlobalStyleProvider>
<GlobalStyleProvider>
{getLayout(<Component {...pageProps} />, pageProps)}
<SpeedInsights />
<Analytics />
</GlobalStyleProvider>
);
}

Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/pages/api/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isRecord } from 'shared-core/helpers/typeguards';
import { echoStravaChallengeIfValid } from 'api/strava/echoStravaChallengeIfValid';
import { syncStravaWebhookUpdateWithDb } from 'api/strava/syncStravaWebhookUpdateWithDb';
import type { StravaWebhookEvent } from 'api/strava/StravaWebhookEvent';
import { log } from '@logtail/next';
import { handleApiError, methodNotAllowedError } from 'api/handleApiError';

// Just a shorthand for this function type
Expand All @@ -13,6 +14,7 @@ type AsyncProcessor = (request: NextApiRequest, response: NextApiResponse) => Pr
* Checks most data to see if it's a webhook event
*/
const isWebhookEvent = (body: unknown): body is StravaWebhookEvent => {
log.info('Is this a webhook event?', { body });
if (!isRecord(body)) {
return false;
}
Expand All @@ -28,6 +30,7 @@ const isWebhookEvent = (body: unknown): body is StravaWebhookEvent => {
* fail with a 400.
*/
const handleGet: Processor = (request, response) => {
log.info('Received Strava webhook GET');
if (echoStravaChallengeIfValid(request, response)) {
return;
}
Expand All @@ -39,12 +42,15 @@ const handleGet: Processor = (request, response) => {
* receipt of it.
*/
const handleWebhookEvent: AsyncProcessor = async (request, response) => {
log.info('Received Strava webhook event');
if (!isWebhookEvent(request.body)) {
log.error('Received invalid Strava webhook event');
handleApiError(response, 'Bad Request', 400);
return;
}

const webhookEvent = request.body;
log.info('Got webhook event', { webhookEvent });
switch (webhookEvent.object_type) {
case 'athlete':
// We don't handle auth/deauth events
Expand Down
4 changes: 2 additions & 2 deletions apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": "src"
"baseUrl": "src",
},
"include": ["src", "next.config.js", "next-env.d.ts"]
"include": ["src", "next.config.js", "next-env.d.ts"],
}
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
},
"dependencies": {
"dotenv-mono": "1.3.13",
"lefthook": "1.5.5",
"vercel": "33.0.1"
"lefthook": "1.5.7",
"vercel": "33.1.0"
},
"devDependencies": {
"prettier": "3.1.1",
"prettier": "3.2.4",
"tsconfig": "workspace:*",
"turbo": "1.11.2",
"turbo": "1.11.3",
"typescript": "5.3.3"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"packageManager": "pnpm@8.7.6"
"packageManager": "pnpm@8.14.1"
}
8 changes: 4 additions & 4 deletions packages/api-clients/RefreshTokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export type RefreshTokenConfig = Readonly<{
endpoint: string;

/**
* This gets encoded into body if existent
* This gets encoded into body
*/
data?: Record<string, string | undefined>;
body: (refreshToken: string) => Record<string, string>;

/**
* This gets encoded into headers if existent
* This gets encoded into headers
*/
headers?: Record<string, string>;
headers: Record<string, string>;

/**
* Throws an error if anything's off about our data, otherwise returns the data
Expand Down
3 changes: 2 additions & 1 deletion packages/api-clients/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
"lint:types": "tsc"
},
"dependencies": {
"@logtail/next": "0.1.4",
"db": "workspace:*",
"graphql-request": "6.1.0",
"wretch": "2.8.0"
},
"devDependencies": {
"@types/node": "20.10.6",
"@types/node": "20.11.5",
"dotenv-mono": "1.3.13",
"eslint-config-dg": "workspace:*",
"tsconfig": "workspace:*",
Expand Down
31 changes: 19 additions & 12 deletions packages/api-clients/refreshedAccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { db } from 'db';
import type { CreateTokenProps, FetchTokenProps } from 'db/models/Token';
import { log } from '@logtail/next';
import type { RefreshTokenConfig } from './RefreshTokenConfig';

/**
Expand Down Expand Up @@ -45,25 +46,31 @@ async function fetchRefreshedTokenFromApi(
refreshTokenConfig: RefreshTokenConfig,
refreshToken: string,
) {
const { endpoint, headers, data, validate } = refreshTokenConfig;
const { endpoint, headers, body, validate } = refreshTokenConfig;
const encodedBody = new URLSearchParams(body(refreshToken));
log.info('Fetching refreshed token from API', {
endpoint,
headers,
encodedBody: [...encodedBody],
});

const rawData = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
...headers,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
...data,
}),
headers,
body: encodedBody,
});
const data: unknown = await rawData.json();
if (!rawData.ok) {
log.error('Failed to fetch refreshed token', {
status: rawData.status,
data,
});
throw new TypeError('Token was not fetched properly');
}
return validate(await rawData.json(), refreshToken);
log.info('Successfully refreshed token!', {
status: rawData.status,
});
return validate(data, refreshToken);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/api-clients/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "tsconfig/library.json",
"compilerOptions": {
"baseUrl": "."
}
"baseUrl": ".",
},
}
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
"lint:types": "tsc"
},
"dependencies": {
"@logtail/next": "0.1.4",
"api-clients": "workspace:*",
"db": "workspace:*",
"graphql-request": "6.1.0",
"shared-core": "workspace:*"
},
"devDependencies": {
"@types/node": "20.10.6",
"@types/node": "20.11.5",
"dotenv-mono": "1.3.13",
"eslint-config-dg": "workspace:*",
"tsconfig": "workspace:*",
Expand Down
6 changes: 6 additions & 0 deletions packages/api/spotify/spotifyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,14 @@ export const spotifyClient = createClient({
refreshTokenConfig: {
endpoint: 'https://accounts.spotify.com/api/token/',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
Authorization: `Basic ${SPOTIFY_CLIENT_AUTH}`,
},
body: (refreshToken) => ({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
validate: (rawData, refreshToken) => {
const { token_type: tokenType, access_token: accessToken, expires_in: expiresIn } =
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Expand Down
13 changes: 10 additions & 3 deletions packages/api/strava/stravaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@ export const stravaClient = createClient({
endpoint: 'https://www.strava.com/api/v3/',
accessKey: STRAVA_TOKEN_NAME,
refreshTokenConfig: {
endpoint: 'https://www.strava.com/api/v3/oauth/token/',
data: {
// Note: it's REALLY FUCKING IMPORTANT that this doesn't have a slash at the end. It returns empty otherwise
endpoint: 'https://www.strava.com/api/v3/oauth/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: (refreshToken) => ({
client_id: STRAVA_CLIENT_ID,
client_secret: STRAVA_CLIENT_SECRET,
},
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
validate: (rawData) => {
const {
token_type: tokenType,
Expand Down
8 changes: 8 additions & 0 deletions packages/api/strava/syncStravaWebhookUpdateWithDb.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { db } from 'db';
import { log } from '@logtail/next';
import type { StravaWebhookEvent } from './StravaWebhookEvent';
import { fetchStravaActivityFromApi } from './fetchStravaActivityFromApi';

Expand All @@ -19,12 +20,18 @@ const fetchFromApiAndSaveToDb = async (id: number) => {
existingActivity?.lastUpdate &&
Number(existingActivity.lastUpdate) > Date.now() - UPDATE_THRESHOLD_IN_MS
) {
log.info('Dropping Strava webhook update - last update was too recent', {
id,
lastUpdate: existingActivity.lastUpdate,
threshold: Date.now() - UPDATE_THRESHOLD_IN_MS,
});
// Last update was too recent
return;
}

const latestActivityData = await fetchStravaActivityFromApi(id);
if (!latestActivityData?.start_date) {
log.error('Missing activity data', { id, latestActivityData });
throw new Error(`Missing activity data for ${id}`);
}
const updatedActivity = await db.StravaActivity.upsert({
Expand All @@ -33,6 +40,7 @@ const fetchFromApiAndSaveToDb = async (id: number) => {
activityData: latestActivityData,
lastUpdate: new Date(),
});
log.info('Upserted Strava activity', { updatedActivity });
return updatedActivity[0];
};

Expand Down
4 changes: 2 additions & 2 deletions packages/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "tsconfig/library.json",
"compilerOptions": {
"baseUrl": "."
}
"baseUrl": ".",
},
}
2 changes: 1 addition & 1 deletion packages/codegen/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "tsconfig/library.json"
"extends": "tsconfig/library.json",
}
5 changes: 4 additions & 1 deletion packages/db/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { env } from 'node:process';
import { Sequelize } from 'sequelize-typescript';
import mysql2 from 'mysql2';
import { log } from '@logtail/next';
import { Token } from './models/Token';
import { StravaActivity } from './models/StravaActivity';

Expand All @@ -10,6 +11,7 @@ const SHARED_DB_OPTIONS = {
};

const databaseUrl = env.DATABASE_URL;
log.info('Database url', { databaseUrl });
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable not set');
}
Expand All @@ -18,7 +20,8 @@ if (!databaseUrl) {
const isRunningLocalProdBuild = ['127.0.0.1', 'localhost'].some((host) =>
databaseUrl.includes(host),
);
const nodeEnv = isRunningLocalProdBuild ? 'development' : env.NODE_ENV || 'development';
const nodeEnv = isRunningLocalProdBuild ? 'development' : env.NODE_ENV;
log.info('Node env', { nodeEnv });

// Sequelize options applied based on current environment
const DB_OPTIONS = {
Expand Down
3 changes: 2 additions & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
"lint:types": "tsc"
},
"dependencies": {
"@logtail/next": "0.1.4",
"dotenv-mono": "1.3.13",
"mysql2": "3.6.5",
"mysql2": "3.7.1",
"sequelize": "6.35.2",
"sequelize-typescript": "2.1.6"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/db/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "tsconfig/library.json",
"compilerOptions": {
"baseUrl": "."
}
"baseUrl": ".",
},
}
Loading

1 comment on commit 3dd4991

@vercel
Copy link

@vercel vercel bot commented on 3dd4991 Jan 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

dg – ./

dg-git-main-dgattey.vercel.app
dg-dgattey.vercel.app
dg.vercel.app
dylangattey.com

Please sign in to comment.