Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sentry): add sentry logging to all firebase calls #1755

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 96 additions & 52 deletions tavla/app/(admin)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TavlaError } from './utils/types'
import { redirect } from 'next/navigation'
import { FIREBASE_DEV_CONFIG, FIREBASE_PRD_CONFIG } from './utils/constants'
import { userInOrganization } from './utils'
import * as Sentry from '@sentry/nextjs'

initializeAdminApp()

Expand All @@ -24,7 +25,18 @@ export async function getFirebaseClientConfig() {

export async function getOrganizationIfUserHasAccess(oid?: TOrganizationID) {
if (!oid) return undefined
const doc = await firestore().collection('organizations').doc(oid).get()

let doc = null

try {
doc = await firestore().collection('organizations').doc(oid).get()
if (!doc) throw Error('Fetch org returned null or undefined')
} catch (error) {
Sentry.captureMessage(
'Error while fetching organization from firestore, orgID: ' + oid,
)
throw error
}

const organization = { ...doc.data(), id: doc.id } as TOrganization
const user = await getUserFromSessionCookie()
Expand All @@ -37,22 +49,29 @@ export async function getOrganizationsForUser() {
const user = await getUserFromSessionCookie()
if (!user) return redirect('/')

const owner = firestore()
.collection('organizations')
.where('owners', 'array-contains', user.uid)
.get()

const editor = firestore()
.collection('organizations')
.where('editors', 'array-contains', user.uid)
.get()

const queries = await Promise.all([owner, editor])
return queries
.map((q) =>
q.docs.map((d) => ({ ...d.data(), id: d.id }) as TOrganization),
try {
const owner = firestore()
.collection('organizations')
.where('owners', 'array-contains', user.uid)
.get()

const editor = firestore()
.collection('organizations')
.where('editors', 'array-contains', user.uid)
.get()

const queries = await Promise.all([owner, editor])
return queries
.map((q) =>
q.docs.map((d) => ({ ...d.data(), id: d.id }) as TOrganization),
)
.flat()
} catch (error) {
Sentry.captureMessage(
'Error while fetching organizations for user with id ' + user.uid,
)
.flat()
throw error
}
}

export async function getBoardsForOrganization(oid: TOrganizationID) {
Expand All @@ -64,20 +83,29 @@ export async function getBoardsForOrganization(oid: TOrganizationID) {

const batchedBoardIDs = chunk(boards, 20)

const boardQueries = batchedBoardIDs.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)

const boardRefs = await Promise.all(boardQueries)
try {
const boardQueries = batchedBoardIDs.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)

return boardRefs
.map((ref) =>
ref.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as TBoard),
const boardRefs = await Promise.all(boardQueries)

return boardRefs
.map((ref) =>
ref.docs.map(
(doc) => ({ id: doc.id, ...doc.data() }) as TBoard,
),
)
.flat()
} catch (error) {
Sentry.captureMessage(
'Error while fetching boards for organization with orgID ' + oid,
)
.flat()
throw error
}
}

export async function getPrivateBoardsForUser() {
SelmaBergstrand marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -94,39 +122,55 @@ export async function getPrivateBoardsForUser() {

const batchedBoardIDs = chunk(boardIDs, 20)

const boardQueries = batchedBoardIDs.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)

const boardRefs = await Promise.all(boardQueries)
try {
const boardQueries = batchedBoardIDs.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)

return boardRefs
.map((ref) =>
ref.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as TBoard),
const boardRefs = await Promise.all(boardQueries)

return boardRefs
.map((ref) =>
ref.docs.map(
(doc) => ({ id: doc.id, ...doc.data() }) as TBoard,
),
)
.flat()
} catch (error) {
Sentry.captureMessage(
'Error while fetching private boards for user: ' + user.uid,
)
.flat()
throw error
}
}

export async function getBoards(ids?: TBoardID[]) {
if (!ids) return []

const batches = chunk(ids, 20)
const queries = batches.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)

const refs = await Promise.all(queries)
return refs
.map((ref) =>
ref.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as TBoard),
try {
const queries = batches.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)
.flat()

const refs = await Promise.all(queries)
return refs
.map((ref) =>
ref.docs.map(
(doc) => ({ id: doc.id, ...doc.data() }) as TBoard,
),
)
.flat()
} catch (error) {
Sentry.captureMessage('Error while fetching list of boards: ' + ids)
throw error
}
}

export async function getAllBoardsForUser() {
Expand Down
37 changes: 24 additions & 13 deletions tavla/app/(admin)/boards/components/TagModal/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use server'
import { TFormFeedback, getFormFeedbackForError } from 'app/(admin)/utils'
import { FirebaseError } from 'firebase/app'
import { isString, uniq } from 'lodash'
import { uniq } from 'lodash'
import { revalidatePath } from 'next/cache'
import { hasBoardEditorAccess } from 'app/(admin)/utils/firebase'
import { TBoardID } from 'types/settings'
Expand All @@ -10,6 +9,8 @@ import { TTag } from 'types/meta'
import { isEmptyOrSpaces } from 'app/(admin)/edit/utils'
import { getBoard } from 'Board/scenarios/Board/firebase'
import { notFound } from 'next/navigation'
import * as Sentry from '@sentry/nextjs'
import { handleError } from 'app/(admin)/utils/handleError'

async function fetchTags({ bid }: { bid: TBoardID }) {
const board = await getBoard(bid)
Expand All @@ -32,18 +33,23 @@ export async function removeTag(

const access = await hasBoardEditorAccess(bid)
if (!access) throw 'auth/operation-not-allowed'
SelmaBergstrand marked this conversation as resolved.
Show resolved Hide resolved
const tags = await fetchTags({ bid })

try {
const tags = await fetchTags({ bid })
await firestore()
.collection('boards')
.doc(bid)
.update({ 'meta.tags': tags.filter((t) => t !== tag) })
revalidatePath('/')
} catch (e) {
if (e instanceof FirebaseError || isString(e))
return getFormFeedbackForError(e)
return getFormFeedbackForError('general')
} catch (error) {
Sentry.captureException(error, {
extra: {
message: 'Error while removing tag from firestore',
boardID: bid,
tagValue: tag,
},
})
return handleError(error)
}
}

Expand All @@ -60,9 +66,9 @@ export async function addTag(
const access = await hasBoardEditorAccess(bid)
if (!access) throw 'auth/operation-not-allowed'

try {
const tags = await fetchTags({ bid })
const tags = await fetchTags({ bid })

try {
if (tags.map((t) => t.toUpperCase()).includes(tag.toUpperCase()))
throw 'boards/tag-exists'

Expand All @@ -71,9 +77,14 @@ export async function addTag(
.doc(bid)
.update({ 'meta.tags': uniq([...tags, tag]).sort() })
revalidatePath('/')
} catch (e) {
if (e instanceof FirebaseError || isString(e))
return getFormFeedbackForError(e)
return getFormFeedbackForError('general')
} catch (error) {
Sentry.captureException(error, {
extra: {
message: 'Error while adding new tag to firestore',
boardID: bid,
tagValue: tag,
},
})
return handleError(error)
}
}
9 changes: 3 additions & 6 deletions tavla/app/(admin)/boards/utils/actions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
'use server'
import { TFormFeedback, getFormFeedbackForError } from 'app/(admin)/utils'
import { FirebaseError } from 'firebase/app'
import { isString } from 'lodash'
import { TFormFeedback } from 'app/(admin)/utils'
import { revalidatePath } from 'next/cache'
import { deleteBoard, initializeAdminApp } from 'app/(admin)/utils/firebase'
import { redirect } from 'next/navigation'
import { handleError } from 'app/(admin)/utils/handleError'

initializeAdminApp()

Expand All @@ -18,9 +17,7 @@ export async function deleteBoardAction(
await deleteBoard(bid)
revalidatePath('/')
} catch (e) {
if (e instanceof FirebaseError || isString(e))
return getFormFeedbackForError(e)
return getFormFeedbackForError('general')
return handleError(e)
}
redirect('/boards')
}
50 changes: 30 additions & 20 deletions tavla/app/(admin)/components/CreateBoard/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import { getOrganizationIfUserHasAccess } from 'app/(admin)/actions'
import { TFormFeedback, getFormFeedbackForError } from 'app/(admin)/utils'
import { initializeAdminApp } from 'app/(admin)/utils/firebase'
import { handleError } from 'app/(admin)/utils/handleError'
import { getUserFromSessionCookie } from 'app/(admin)/utils/server'
import admin, { firestore } from 'firebase-admin'
import { FirebaseError } from 'firebase/app'
import { redirect } from 'next/navigation'
import { TBoard, TOrganization, TOrganizationID } from 'types/settings'
import * as Sentry from '@sentry/nextjs'

initializeAdminApp()

Expand Down Expand Up @@ -35,34 +36,43 @@ export async function createBoard(
let organization: TOrganization | undefined
if (oid) organization = await getOrganizationIfUserHasAccess(oid)

const createdBoard = await firestore()
.collection('boards')
.add({
...board,
meta: {
...board.meta,
fontSize:
board.meta?.fontSize ??
organization?.defaults?.font ??
'medium',
created: Date.now(),
dateModified: Date.now(),
},
})

if (!createdBoard) return getFormFeedbackForError('firebase/general')
let createdBoard = null
emilielr marked this conversation as resolved.
Show resolved Hide resolved

try {
createdBoard = await firestore()
.collection('boards')
.add({
...board,
meta: {
...board.meta,
fontSize:
board.meta?.fontSize ??
organization?.defaults?.font ??
'medium',
created: Date.now(),
dateModified: Date.now(),
},
})

if (!createdBoard) return getFormFeedbackForError('firebase/general')

firestore()
.collection(oid ? 'organizations' : 'users')
.doc(oid ? String(oid) : String(user.uid))
.update({
[oid ? 'boards' : 'owner']:
admin.firestore.FieldValue.arrayUnion(createdBoard.id),
})
} catch (e) {
if (e instanceof FirebaseError) return getFormFeedbackForError(e)
return getFormFeedbackForError('firebase/general')
} catch (error) {
Sentry.captureException(error, {
extra: {
message:
'Error while adding newly created board to either user or org',
userID: user.uid,
orgID: oid,
},
})
return handleError(error)
}

redirect(`/edit/${createdBoard.id}`)
emilielr marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
6 changes: 2 additions & 4 deletions tavla/app/(admin)/components/Login/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import {
getFormFeedbackForError,
getFormFeedbackForField,
} from 'app/(admin)/utils'
import { FirebaseError } from 'firebase/app'
import { FormError } from '../FormError'
import { SubmitButton } from 'components/Form/SubmitButton'
import { usePathname } from 'next/navigation'
import { Button, ButtonGroup } from '@entur/button'
import Link from 'next/link'
import ClientOnlyTextField from 'app/components/NoSSR/TextField'
import { handleError } from 'app/(admin)/utils/handleError'

function Create() {
const submit = async (p: TFormFeedback | undefined, data: FormData) => {
Expand All @@ -44,9 +44,7 @@ function Create() {
await sendEmailVerification(credential.user)
return getFormFeedbackForError('auth/create', email)
} catch (e) {
if (e instanceof FirebaseError) {
return getFormFeedbackForError(e)
}
return handleError(e)
}
}
const [state, action] = useActionState(submit, undefined)
Expand Down
Loading