Skip to content

Commit

Permalink
feat(sentry): add sentry logging to all firebase calls (#1755)
Browse files Browse the repository at this point in the history
* feat(sentry): add sentry logging to all firebase calls

* fix(create): re-throw error if creating user in firebase does not work

* chore(sentry): remove unused function and fix typos in error messages

* chore(sentry): fix getBoard captureMessage

* chore(sentry): fix merge conflicts from logo PR
  • Loading branch information
SelmaBergstrand authored Dec 17, 2024
1 parent 4454656 commit 0daa9ff
Show file tree
Hide file tree
Showing 26 changed files with 591 additions and 329 deletions.
144 changes: 74 additions & 70 deletions tavla/app/(admin)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
import { getUser, initializeAdminApp } from './utils/firebase'
import { getUserFromSessionCookie } from './utils/server'
import { chunk, concat, isEmpty, flattenDeep } from 'lodash'
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 +24,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 +48,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,69 +82,55 @@ 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)

return boardRefs
.map((ref) =>
ref.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as TBoard),
try {
const boardQueries = batchedBoardIDs.map((batch) =>
firestore()
.collection('boards')
.where(firestore.FieldPath.documentId(), 'in', batch)
.get(),
)
.flat()
}

export async function getPrivateBoardsForUser() {
const user = await getUser()
if (!user)
throw new TavlaError({
code: 'NOT_FOUND',
message: `Found no user`,
})

const boardIDs = concat(user.owner ?? [], user.editor ?? [])

if (isEmpty(boardIDs)) return []

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)

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 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
43 changes: 28 additions & 15 deletions tavla/app/(admin)/boards/components/TagModal/actions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
'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'
import { firestore } from 'firebase-admin'
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 { notFound, redirect } 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 @@ -18,7 +19,9 @@ async function fetchTags({ bid }: { bid: TBoardID }) {
}

const access = await hasBoardEditorAccess(board.id)
if (!access) throw 'auth/operation-not-allowed'
if (!access) {
redirect('/')
}

return (board?.meta?.tags as TTag[]) ?? []
}
Expand All @@ -32,18 +35,23 @@ export async function removeTag(

const access = await hasBoardEditorAccess(bid)
if (!access) throw 'auth/operation-not-allowed'
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 +68,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 +79,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

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}`)
Expand Down
Loading

0 comments on commit 0daa9ff

Please sign in to comment.