Translations for Email Notifications (#1278)

Co-authored-by: Niko Heller <hellerniko@gmail.com>
This commit is contained in:
Luke Vella 2024-09-02 19:30:58 +01:00 committed by GitHub
parent aa52a0f26f
commit f4218c3115
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1071 additions and 970 deletions

View file

@ -1,9 +1,12 @@
import "i18next";
import emails from "@rallly/emails/locales/emails.json";
import app from "../public/locales/en/app.json";
interface I18nNamespaces {
app: typeof app;
emails: typeof emails;
}
declare module "i18next" {

View file

@ -31,7 +31,8 @@
"@rallly/languages": "*",
"@rallly/tailwind-config": "*",
"@rallly/ui": "*",
"@sentry/nextjs": "^8",
"@rallly/emails": "*",
"@sentry/nextjs": "*",
"@svgr/webpack": "^6.5.1",
"@t3-oss/env-nextjs": "^0.11.0",
"@tanstack/react-query": "^4.0.0",

View file

@ -1,100 +0,0 @@
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import { HelpCircleIcon } from "lucide-react";
import Script from "next/script";
import React from "react";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { isFeedbackEnabled } from "@/utils/constants";
const FeaturebaseScript = () => (
<Script src="https://do.featurebase.app/js/sdk.js" id="featurebase-sdk" />
);
export const FeaturebaseChangelog = ({ className }: { className?: string }) => {
React.useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window as any;
if (typeof win.Featurebase !== "function") {
win.Featurebase = function (...args: unknown[]) {
(win.Featurebase.q = win.Featurebase.q || []).push(args);
};
}
win.Featurebase("initialize_changelog_widget", {
organization: "rallly", // Replace this with your featurebase organization name
placement: "bottom", // Choose between right, left, top, bottom placement
theme: "light", // Choose between dark or light theme
});
}, []);
if (!isFeedbackEnabled) return null;
return (
<>
<FeaturebaseScript />
<Tooltip>
<TooltipTrigger asChild>
<Button
className={cn(
"hidden sm:inline-flex [&>*]:pointer-events-none",
className,
)}
size="sm"
variant="ghost"
data-featurebase-changelog
>
<HelpCircleIcon className="size-4" />
<span
id="fb-update-badge"
className="bg-primary rounded-full px-2 py-px text-xs text-gray-100 empty:hidden"
/>
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans key="whatsNew" defaults="What's new?" />
</TooltipContent>
</Tooltip>
</>
);
};
export const FeaturebaseIdentify = () => {
const { user } = useUser();
React.useEffect(() => {
if (user?.isGuest !== false || !isFeedbackEnabled) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window as any;
if (typeof win.Featurebase !== "function") {
win.Featurebase = function () {
// eslint-disable-next-line prefer-rest-params
(win.Featurebase.q = win.Featurebase.q || []).push(arguments);
};
}
win.Featurebase(
"identify",
{
organization: "rallly",
// Required. Replace with your customers data.
email: user.email,
name: user.name,
id: user.id,
},
(err: Error) => {
// Callback function. Called when identify completed.
if (err) {
console.error(err);
}
},
);
}, [user]);
return <FeaturebaseScript />;
};

View file

@ -2,9 +2,15 @@ import { Trans as BaseTrans } from "react-i18next";
import { useTranslation } from "@/app/i18n/client";
type TransWithContextProps = Omit<React.ComponentProps<typeof BaseTrans>, "t">;
import { I18nNamespaces } from "../../declarations/i18next";
export const Trans = (props: TransWithContextProps) => {
const { t } = useTranslation();
return <BaseTrans t={t} {...props} />;
export const Trans = (props: {
i18nKey: keyof I18nNamespaces["app"];
defaults?: string;
values?: Record<string, string | number | boolean | undefined>;
children?: React.ReactNode;
components?: Record<string, React.ReactElement> | React.ReactElement[];
}) => {
const { t } = useTranslation("app");
return <BaseTrans ns="app" t={t} {...props} />;
};

View file

@ -10,7 +10,7 @@ import { posthog, posthogApiHandler } from "@/app/posthog";
import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
import { getServerSession, isEmailBlocked } from "@/utils/auth";
import { isSelfHosted } from "@/utils/constants";
import { emailClient } from "@/utils/emails";
import { getEmailClient } from "@/utils/emails";
import { composeApiHandlers } from "@/utils/next";
const ratelimit = new Ratelimit({
@ -39,13 +39,15 @@ const trpcApiHandler = createNextApiHandler<AppRouter>({
id: session.user.id,
isGuest: session.user.email === null,
locale: session.user.locale ?? undefined,
getEmailClient: () =>
getEmailClient(session.user.locale ?? undefined),
};
},
posthogClient: posthog || undefined,
emailClient,
isSelfHosted,
isEmailBlocked,
absoluteUrl,
getEmailClient,
shortUrl,
ratelimit: async () => {
if (!process.env.KV_REST_API_URL) {

View file

@ -22,7 +22,7 @@ import { env } from "@/env";
import { absoluteUrl } from "@/utils/absolute-url";
import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter";
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
import { emailClient } from "@/utils/emails";
import { getEmailClient } from "@/utils/emails";
import { getValueByPath } from "@/utils/get-value-by-path";
const providers: Provider[] = [
@ -90,21 +90,23 @@ const providers: Provider[] = [
},
select: {
name: true,
locale: true,
},
});
if (user) {
await emailClient.sendTemplate("LoginEmail", {
to: email,
subject: `${token} is your 6-digit code`,
props: {
name: user.name,
magicLink: absoluteUrl("/auth/login", {
magicLink: url,
}),
code: token,
await getEmailClient(user.locale ?? undefined).sendTemplate(
"LoginEmail",
{
to: email,
props: {
magicLink: absoluteUrl("/auth/login", {
magicLink: url,
}),
code: token,
},
},
});
);
}
},
}),

View file

@ -1,26 +1,34 @@
import { EmailClient, SupportedEmailProviders } from "@rallly/emails";
import * as Sentry from "@sentry/nextjs";
import { env } from "@/env";
import { absoluteUrl } from "@/utils/absolute-url";
import { isSelfHosted } from "@/utils/constants";
export const emailClient = new EmailClient({
openPreviews: env.NODE_ENV === "development",
provider: {
name: (process.env.EMAIL_PROVIDER as SupportedEmailProviders) ?? "smtp",
},
mail: {
from: {
name: env.NOREPLY_EMAIL_NAME,
address: env.NOREPLY_EMAIL || env.SUPPORT_EMAIL,
export const getEmailClient = (locale?: string) => {
return new EmailClient({
openPreviews: env.NODE_ENV === "development",
provider: {
name: (process.env.EMAIL_PROVIDER as SupportedEmailProviders) ?? "smtp",
},
},
context: {
logoUrl: isSelfHosted
? absoluteUrl("/images/rallly-logo-mark.png")
: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
baseUrl: absoluteUrl(""),
domain: absoluteUrl("").replace(/(^\w+:|^)\/\//, ""),
supportEmail: env.SUPPORT_EMAIL,
},
});
mail: {
from: {
name: env.NOREPLY_EMAIL_NAME,
address: env.NOREPLY_EMAIL || env.SUPPORT_EMAIL,
},
},
config: {
logoUrl: isSelfHosted
? absoluteUrl("/images/rallly-logo-mark.png")
: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
baseUrl: absoluteUrl(),
domain: absoluteUrl().replace(/(^\w+:|^)\/\//, ""),
supportEmail: env.SUPPORT_EMAIL,
},
locale,
onError: (e) => {
console.error(e);
Sentry.captureException(e);
},
});
};

View file

@ -4,7 +4,6 @@
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["public/*"],
},
"strictNullChecks": true,
},
@ -20,5 +19,6 @@
".next/**/*",
"playwright-report",
"test-results",
"../../packages/emails/src/templates/*.tsx",
],
}

View file

@ -6,3 +6,5 @@ files:
translation: /apps/web/public/locales/%two_letters_code%/%original_file_name%
- source: /apps/landing/public/locales/en/*.json
translation: /apps/landing/public/locales/%two_letters_code%/%original_file_name%
- source: /packages/emails/locales/en/*.json
translation: /packages/emails/locales/%two_letters_code%/%original_file_name%

View file

@ -33,7 +33,7 @@
],
"dependencies": {
"@prisma/client": "^5.17.0",
"@sentry/nextjs": "^8.10.0",
"@sentry/nextjs": "^8.27.0",
"framer-motion": "^10.16.4",
"next": "^14.2.6",
"react": "^18.2.0",

View file

@ -7,14 +7,15 @@ export type GetUserFn = (opts: CreateNextContextOptions) => Promise<{
id: string;
isGuest: boolean;
locale?: string;
getEmailClient: (locale?: string) => EmailClient;
} | null>;
export interface TRPCContextParams {
getUser: GetUserFn;
emailClient: EmailClient;
isSelfHosted: boolean;
isEmailBlocked?: (email: string) => boolean;
posthogClient?: PostHog;
getEmailClient: (locale?: string) => EmailClient;
/**
* Takes a relative path and returns an absolute URL to the app
* @param path

View file

@ -48,9 +48,8 @@ export const auth = router({
code,
});
await ctx.emailClient.sendTemplate("RegisterEmail", {
await ctx.user.getEmailClient().sendTemplate("RegisterEmail", {
to: input.email,
subject: "Please verify your email address",
props: {
code,
},

View file

@ -1,6 +1,5 @@
import { PollStatus, prisma } from "@rallly/database";
import { TRPCError } from "@trpc/server";
import { waitUntil } from "@vercel/functions";
import dayjs from "dayjs";
import * as ics from "ics";
import { z } from "zod";
@ -234,18 +233,15 @@ export const polls = router({
});
if (user) {
waitUntil(
ctx.emailClient.sendTemplate("NewPollEmail", {
to: user.email,
subject: `Let's find a date for ${poll.title}`,
props: {
title: poll.title,
name: user.name,
adminLink: pollLink,
participantLink,
},
}),
);
ctx.user.getEmailClient().queueTemplate("NewPollEmail", {
to: user.email,
props: {
title: poll.title,
name: user.name,
adminLink: pollLink,
participantLink,
},
});
}
}
@ -746,6 +742,7 @@ export const polls = router({
select: {
name: true,
email: true,
locale: true,
votes: {
select: {
optionId: true,
@ -890,7 +887,11 @@ export const polls = router({
? `${startTime} - ${endTime} ${timeZoneAbbrev}`
: "All-day";
const participantsToEmail: Array<{ name: string; email: string }> = [];
const participantsToEmail: Array<{
name: string;
email: string;
locale: string | undefined;
}> = [];
if (input.notify === "all") {
poll.participants.forEach((p) => {
@ -898,6 +899,7 @@ export const polls = router({
participantsToEmail.push({
name: p.name,
email: p.email,
locale: p.locale ?? undefined,
});
}
});
@ -909,62 +911,52 @@ export const polls = router({
participantsToEmail.push({
name: p.name,
email: p.email,
locale: p.locale ?? undefined,
});
}
});
}
const emailToHost = waitUntil(
ctx.emailClient.sendTemplate("FinalizeHostEmail", {
subject: `Date booked for ${poll.title}`,
to: poll.user.email,
props: {
name: poll.user.name,
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
location: poll.location,
title: poll.title,
attendees: poll.participants
.filter((p) =>
p.votes.some(
(v) => v.optionId === input.optionId && v.type !== "no",
),
)
.map((p) => p.name),
date,
day,
dow,
time,
},
attachments: [{ filename: "event.ics", content: event.value }],
}),
);
const emailsToParticipants = participantsToEmail.map((p) => {
return ctx.emailClient.sendTemplate("FinalizeParticipantEmail", {
subject: `Date booked for ${poll.title}`,
to: p.email,
props: {
name: p.name,
pollUrl: ctx.absoluteUrl(`/invite/${poll.id}`),
location: poll.location,
title: poll.title,
hostName: poll.user?.name ?? "",
attendees: poll.participants
.filter((p) =>
p.votes.some(
(v) => v.optionId === input.optionId && v.type !== "no",
),
)
.map((p) => p.name),
date,
day,
dow,
time,
},
attachments: [{ filename: "event.ics", content: event.value }],
});
ctx.user.getEmailClient().queueTemplate("FinalizeHostEmail", {
to: poll.user.email,
props: {
name: poll.user.name,
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
location: poll.location,
title: poll.title,
attendees: poll.participants
.filter((p) =>
p.votes.some(
(v) => v.optionId === input.optionId && v.type !== "no",
),
)
.map((p) => p.name),
date,
day,
dow,
time,
},
attachments: [{ filename: "event.ics", content: event.value }],
});
for (const p of participantsToEmail) {
ctx
.getEmailClient(p.locale ?? undefined)
.queueTemplate("FinalizeParticipantEmail", {
to: p.email,
props: {
pollUrl: ctx.absoluteUrl(`/invite/${poll.id}`),
title: poll.title,
hostName: poll.user?.name ?? "",
date,
day,
dow,
time,
},
attachments: [{ filename: "event.ics", content: event.value }],
});
}
ctx.posthogClient?.capture({
distinctId: ctx.user.id,
event: "finalize poll",
@ -976,8 +968,6 @@ export const polls = router({
days_since_created: dayjs().diff(poll.createdAt, "day"),
},
});
waitUntil(Promise.all([emailToHost, ...emailsToParticipants]));
}
}),
reopen: possiblyPublicProcedure

View file

@ -64,6 +64,7 @@ export const comments = router({
select: {
email: true,
name: true,
locale: true,
},
},
},
@ -71,20 +72,17 @@ export const comments = router({
const poll = newComment.poll;
const emailsToSend: Promise<void>[] = [];
for (const watcher of watchers) {
const email = watcher.user.email;
const token = await createToken<DisableNotificationsPayload>(
{ watcherId: watcher.id, pollId },
{ ttl: 0 },
);
emailsToSend.push(
ctx.emailClient.sendTemplate("NewCommentEmail", {
ctx
.getEmailClient(watcher.user.locale ?? undefined)
.queueTemplate("NewCommentEmail", {
to: email,
subject: `${authorName} has commented on ${poll.title}`,
props: {
name: watcher.user.name,
authorName,
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
disableNotificationsUrl: ctx.absoluteUrl(
@ -92,8 +90,7 @@ export const comments = router({
),
title: poll.title,
},
}),
);
});
}
return newComment;

View file

@ -1,6 +1,5 @@
import { prisma } from "@rallly/database";
import { TRPCError } from "@trpc/server";
import { waitUntil } from "@vercel/functions";
import { z } from "zod";
import { createToken } from "../../../session";
@ -91,6 +90,7 @@ export const participants = router({
name: name,
email,
userId: user.id,
locale: user.locale ?? undefined,
},
});
@ -106,7 +106,6 @@ export const participants = router({
return participant;
});
const emailsToSend: Promise<void>[] = [];
if (email) {
const token = await createToken(
{ userId: user.id },
@ -115,19 +114,17 @@ export const participants = router({
},
);
emailsToSend.push(
ctx.emailClient.sendTemplate("NewParticipantConfirmationEmail", {
ctx.user
.getEmailClient()
.queueTemplate("NewParticipantConfirmationEmail", {
to: email,
subject: `Thanks for responding to ${poll.title}`,
props: {
name,
title: poll.title,
editSubmissionUrl: ctx.absoluteUrl(
`/invite/${poll.id}?token=${token}`,
),
},
}),
);
});
}
const watchers = await prisma.watcher.findMany({
@ -152,25 +149,19 @@ export const participants = router({
{ watcherId: watcher.id, pollId },
{ ttl: 0 },
);
emailsToSend.push(
ctx.emailClient.sendTemplate("NewParticipantEmail", {
to: email,
subject: `${participant.name} has responded to ${poll.title}`,
props: {
name: watcher.user.name,
participantName: participant.name,
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
disableNotificationsUrl: ctx.absoluteUrl(
`/auth/disable-notifications?token=${token}`,
),
title: poll.title,
},
}),
);
ctx.user.getEmailClient().queueTemplate("NewParticipantEmail", {
to: email,
props: {
participantName: participant.name,
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
disableNotificationsUrl: ctx.absoluteUrl(
`/auth/disable-notifications?token=${token}`,
),
title: poll.title,
},
});
}
waitUntil(Promise.all(emailsToSend));
return participant;
}),
rename: publicProcedure

View file

@ -1,5 +1,5 @@
{
"extends": "@rallly/tsconfig/react.json",
"extends": "@rallly/tsconfig/next.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules"],
}

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "participants" ADD COLUMN "locale" TEXT;

View file

@ -186,6 +186,7 @@ model Participant {
poll Poll @relation(fields: [pollId], references: [id])
pollId String @map("poll_id")
votes Vote[]
locale String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
deleted Boolean @default(false)

View file

@ -0,0 +1,29 @@
const typescriptTransform = require("i18next-scanner-typescript");
module.exports = {
input: ["src/templates/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
options: {
nsSeparator: false,
defaultNs: "emails",
defaultValue: "__STRING_NOT_TRANSLATED__",
lngs: ["en"],
ns: ["emails"],
plural: false,
removeUnusedKeys: true,
func: {
list: ["t", "ctx.t"],
},
trans: {
component: "Trans",
i18nKey: "i18nKey",
defaultsKey: "defaults",
},
resource: {
loadPath: "locales/{{lng}}/{{ns}}.json",
savePath: "locales/{{lng}}/{{ns}}.json",
},
},
format: "json",
fallbackLng: "en",
transform: typescriptTransform(),
};

15
packages/emails/i18next.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
import "i18next";
import emails from "./locales/en/emails.json";
interface I18nNamespaces {
emails: typeof emails;
}
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "emails";
resources: I18nNamespaces;
returnNull: false;
}
}

View file

@ -0,0 +1,49 @@
{
"common_poweredBy": "Powered by <a>{{domain}}</a>",
"common_disableNotifications": "If you would like to stop receiving updates you can <a>turn notifications off</a>.",
"finalizeHost_content": "<b>{{title}}</b> has been booked for:",
"finalizeHost_preview": "Final date booked! We've notified participants and sent them calendar invites.",
"finalizeHost_heading": "Final date booked!",
"finalizeHost_content2": "We've notified participants and sent them calendar invites.",
"finalizeHost_button": "View Event",
"finalizeHost_subject": "Date booked for {{title}}",
"finalizeParticipant_content": "<b>{{hostName}}</b> has booked <b>{{title}}</b> for the following date:",
"finalizeParticipant_preview": "Final date booked!",
"finalizeParticipant_heading": "Final date booked!",
"finalizeParticipant_content2": "Please find attached a calendar invite for this event.",
"finalizeParticipant_subject": "Date booked for {{title}}",
"login_button": "Log in to {{domain}}",
"login_content2": "You're receiving this email because a request was made to login to <domain />. If this wasn't you contact <a>{{supportEmail}}</a>.",
"login_preview": "Use this link to log in on this device.",
"login_heading": "Login",
"login_content": "Enter this one-time 6-digit verification code:",
"login_codeValid": "This code is valid for 15 minutes",
"login_subject": "{{code}} is your 6-digit code",
"newComment_content": "<b>{{authorName}}</b> has commented on <b>{{title}}</b>.",
"newComment_subject": "{{authorName}} has commented on {{title}}",
"newParticipantConfirmation_content": "Your response to <b>{{title}}</b> has been submitted.",
"newParticipantConfirmation_content2": "While the poll is still open you can change your response using the link below.",
"newParticipantConfirmation_button": "Review response on {{domain}}",
"newParticipantConfirmation_footnote": "You are receiving this email because a response was submitted on <domain />. If this wasn't you, please ignore this email.",
"newParticipantConfirmation_preview": "To edit your response use the link below",
"newParticipantConfirmation_heading": "Poll Response Confirmation",
"newParticipantConfirmation_subject": "Thanks for responding to {{title}}",
"newParticipant_content": "<b>{{name}}</b> has responded to <b>{{title}}</b>.",
"newParticipant_content2": "Go to your poll to see the new response.",
"newParticipant_preview": "Go to your poll to see the new response.",
"newParticipant_heading": "New Response",
"newParticipant_subject": "{{name}} has responded to {{title}}",
"newPoll_content": "Your meeting poll titled <b>{{title}}</b> is ready! Share it using the link below:",
"newPoll_heading": "New Poll Created",
"newPoll_button": "Manage Poll",
"newPoll_subject": "Let's find a date for {{title}}!",
"register_footer": "You're receiving this email because a request was made to register an account on <domain />. If this wasn't you, please ignore this email.",
"register_preview": "Your 6-digit code is: {{code}}",
"register_heading": "Verify your email address",
"register_text": "Please use the following 6-digit verification code to verify your email",
"register_codeValid": "This code is valid for 15 minutes",
"register_subject": "Please verify your email address",
"common_viewOn": "View on {{domain}}",
"newComment_preview": "Go to your poll to see what they said.",
"newComment_heading": "New Comment"
}

View file

@ -3,12 +3,15 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "email dev --port 3333 --dir ./src/templates",
"dev": "email dev --port 3333 --dir ./src/previews",
"lint": "eslint ./src",
"type-check": "tsc --pretty --noEmit"
"type-check": "tsc --pretty --noEmit",
"i18n:scan": "i18next-scanner --config i18next-scanner.config.js"
},
"exports": {
".": "./src/index.ts",
"./locales/*": "./locales/en/*.json"
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@aws-sdk/client-ses": "^3.501.0",
"@aws-sdk/credential-provider-node": "^3.501.0",
@ -16,7 +19,8 @@
"@react-email/render": "^0.0.12",
"nodemailer": "^6.9.9",
"preview-email": "^3.0.19",
"react-email": "^2.0.0"
"react-email": "^2.0.0",
"@vercel/functions": "*"
},
"devDependencies": {
"@rallly/tailwind-config": "*",

View file

@ -0,0 +1,16 @@
import { i18nDefaultConfig, i18nInstance } from "../i18n";
import { EmailContext } from "../types";
i18nInstance.init({
...i18nDefaultConfig,
initImmediate: true,
});
export const previewEmailContext: EmailContext = {
logoUrl: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
baseUrl: "https://rallly.co",
domain: "rallly.co",
supportEmail: "support@rallly.co",
i18n: i18nInstance,
t: i18nInstance.getFixedT("en"),
};

View file

@ -7,8 +7,9 @@ import {
Preview,
Section,
} from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailContext } from "./email-context";
import { EmailContext } from "../types";
import { darkTextColor, fontFamily, Link, Text } from "./styled-components";
export interface EmailLayoutProps {
@ -47,12 +48,21 @@ export const EmailLayout = ({
alt="Rallly Logo"
/>
{children}
<Section style={{ marginTop: 32, textAlign: "center" }}>
<Section style={{ marginTop: 32 }}>
<Text light={true}>
Powered by{" "}
<Link href="https://rallly.co?utm_source=email&utm_medium=transactional">
rallly.co
</Link>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="common_poweredBy"
ns="emails"
defaults="Powered by <a>{{domain}}</a>"
values={{ domain: "rallly.co" }}
components={{
a: (
<Link href="https://rallly.co?utm_source=email&utm_medium=transactional" />
),
}}
/>
</Text>
</Section>
</Container>

View file

@ -1,11 +1,11 @@
import { Section } from "@react-email/section";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailContext } from "./email-context";
import type { EmailContext } from "../types";
import { EmailLayout } from "./email-layout";
import { Button, Link, Text } from "./styled-components";
export interface NotificationBaseProps {
name: string;
title: string;
pollUrl: string;
disableNotificationsUrl: string;
@ -28,14 +28,30 @@ export const NotificationEmail = ({
<EmailLayout ctx={ctx} preview={preview}>
{children}
<Section style={{ marginTop: 32, marginBottom: 32 }}>
<Button href={pollUrl}>View on {domain}</Button>
<Button href={pollUrl}>
{ctx.t("common_viewOn", {
ns: "emails",
defaultValue: "View on {{domain}}",
domain,
})}
</Button>
</Section>
<Text light={true}>
If you would like to stop receiving updates you can{" "}
<Link className="whitespace-nowrap" href={disableNotificationsUrl}>
turn notifications off
</Link>
.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="common_disableNotifications"
ns="emails"
defaults="If you would like to stop receiving updates you can <a>turn notifications off</a>."
components={{
a: (
<Link
className="whitespace-nowrap"
href={disableNotificationsUrl}
/>
),
}}
/>
</Text>
</EmailLayout>
);

View file

@ -9,7 +9,7 @@ import {
TextProps,
} from "@react-email/components";
import { EmailContext } from "./email-context";
import type { EmailContext } from "../types";
export const lightTextColor = "#4B5563";
export const darkTextColor = "#1F2937";

View file

@ -0,0 +1,26 @@
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
const i18nInstance = createInstance();
i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`../locales/${language}/${namespace}.json`),
),
);
const i18nDefaultConfig = {
lng: "en",
fallbackLng: "en",
ns: ["emails"],
fallbackNS: "emails",
defaultNS: "emails",
} as const;
export type I18nInstance = typeof i18nInstance;
export { i18nDefaultConfig, i18nInstance };

View file

@ -0,0 +1,19 @@
import { previewEmailContext } from "../components/email-context";
import { FinalizeHostEmail } from "../templates/finalized-host";
export default function FinalizedHostPreview() {
return (
<FinalizeHostEmail
name="John Doe"
location="Zoom"
attendees={["johndoe@example.com", "janedoe@example.com"]}
title="Untitled Poll"
pollUrl="https://rallly.co"
day="12"
dow="Fri"
date="Friday, 12th June 2020"
time="6:00 PM to 11:00 PM BST"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,17 @@
import { previewEmailContext } from "../components/email-context";
import { FinalizeParticipantEmail } from "../templates/finalized-participant";
export default function FinalizedParticipantPreview() {
return (
<FinalizeParticipantEmail
title="Untitled Poll"
hostName="Host"
pollUrl="https://rallly.co"
day="12"
dow="Fri"
date="Friday, 12th June 2020"
time="6:00 PM to 11:00 PM BST"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,12 @@
import { previewEmailContext } from "../components/email-context";
import { LoginEmail } from "../templates/login";
export default function LoginPreview() {
return (
<LoginEmail
code="123456"
magicLink="https://rallly.co"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,16 @@
import { previewEmailContext } from "../components/email-context";
import { NewCommentEmail } from "../templates/new-comment";
function NewCommentEmailPreview() {
return (
<NewCommentEmail
title="Untitled Poll"
authorName="Someone"
pollUrl="https://rallly.co"
disableNotificationsUrl="https://rallly.co"
ctx={previewEmailContext}
/>
);
}
export default NewCommentEmailPreview;

View file

@ -0,0 +1,12 @@
import { previewEmailContext } from "../components/email-context";
import NewParticipantConfirmationEmail from "../templates/new-participant-confirmation";
export default function NewParticipantConfirmationPreview() {
return (
<NewParticipantConfirmationEmail
title="Untitled Poll"
editSubmissionUrl="https://rallly.co"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,14 @@
import { previewEmailContext } from "../components/email-context";
import { NewParticipantEmail } from "../templates/new-participant";
export default function NewParticipantPreview() {
return (
<NewParticipantEmail
participantName="John Doe"
title="Untitled Poll"
pollUrl="https://rallly.co"
disableNotificationsUrl="https://rallly.co"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,14 @@
import { previewEmailContext } from "../components/email-context";
import NewPollEmail from "../templates/new-poll";
export default function NewPollPreview() {
return (
<NewPollEmail
title="Untitled Poll"
name="John Doe"
adminLink="https://rallly.co"
participantLink="https://rallly.co/invite/abc123"
ctx={previewEmailContext}
/>
);
}

View file

@ -0,0 +1,6 @@
import { previewEmailContext } from "../components/email-context";
import { RegisterEmail } from "../templates/register";
export default function RegisterEmailPreview() {
return <RegisterEmail code="123456" ctx={previewEmailContext} />;
}

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

@ -1,13 +1,15 @@
import * as aws from "@aws-sdk/client-ses";
import { defaultProvider } from "@aws-sdk/credential-provider-node";
import { renderAsync } from "@react-email/render";
import { waitUntil } from "@vercel/functions";
import { createTransport, Transporter } from "nodemailer";
import type Mail from "nodemailer/lib/mailer";
import previewEmail from "preview-email";
import React from "react";
import { i18nDefaultConfig, i18nInstance } from "./i18n";
import * as templates from "./templates";
import { EmailContext } from "./templates/_components/email-context";
import type { EmailContext } from "./types";
type Templates = typeof templates;
@ -17,11 +19,12 @@ type TemplateProps<T extends TemplateName> = Omit<
React.ComponentProps<TemplateComponent<T>>,
"ctx"
>;
type TemplateComponent<T extends TemplateName> = Templates[T];
type TemplateComponent<T extends TemplateName> = Templates[T] & {
getSubject?: (props: TemplateProps<T>, ctx: EmailContext) => string;
};
type SendEmailOptions<T extends TemplateName> = {
to: string;
subject: string;
props: TemplateProps<T>;
attachments?: Mail.Options["attachments"];
};
@ -59,7 +62,15 @@ type EmailClientConfig = {
/**
* Context to pass to each email
*/
context: EmailContext;
config: {
logoUrl: string;
baseUrl: string;
domain: string;
supportEmail: string;
};
locale?: string;
onError?: (error: Error) => void;
};
export class EmailClient {
@ -70,16 +81,41 @@ export class EmailClient {
this.config = config;
}
queueTemplate<T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) {
return waitUntil(
(async () => {
this.sendTemplate(templateName, options);
})(),
);
}
async sendTemplate<T extends TemplateName>(
templateName: T,
options: SendEmailOptions<T>,
) {
const locale = this.config.locale ?? "en";
await i18nInstance.init({
...i18nDefaultConfig,
lng: locale,
});
const ctx = {
...this.config.config,
i18n: i18nInstance,
t: i18nInstance.getFixedT(locale),
};
const Template = templates[templateName] as TemplateComponent<T>;
const subject = Template.getSubject?.(options.props, ctx);
const component = (
<Template
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(options.props as any)}
ctx={this.config.context}
ctx={ctx}
/>
);
@ -92,13 +128,24 @@ export class EmailClient {
await this.sendEmail({
from: this.config.mail.from,
to: options.to,
subject: options.subject,
subject,
html,
text,
attachments: options.attachments,
});
} catch (e) {
console.error("Error sending email", templateName, e);
const enhancedError = new Error(
`Failed to send email template: ${templateName}`,
{
cause: e instanceof Error ? e : new Error(String(e)),
},
);
Object.assign(enhancedError, {
templateName,
recipient: options.to,
subject,
});
this.config.onError?.(enhancedError);
}
}

View file

@ -1,13 +0,0 @@
export type EmailContext = {
logoUrl: string;
baseUrl: string;
domain: string;
supportEmail: string;
};
export const defaultEmailContext = {
logoUrl: "https://rallly-public.s3.amazonaws.com/images/rallly-logo-mark.png",
baseUrl: "https://rallly.co",
domain: "rallly.co",
supportEmail: "support@rallly.co",
};

View file

@ -1,13 +1,14 @@
import { Column, Row, Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
borderColor,
Button,
Heading,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
export interface FinalizeHostEmailProps {
date: string;
@ -22,20 +23,43 @@ export interface FinalizeHostEmailProps {
ctx: EmailContext;
}
export const FinalizeHostEmail = ({
title = "Untitled Poll",
pollUrl = "https://rallly.co",
day = "12",
dow = "Fri",
date = "Friday, 12th June 2020",
time = "6:00 PM to 11:00 PM BST",
ctx = defaultEmailContext,
const FinalizeHostEmail = ({
title,
pollUrl,
day,
dow,
date,
time,
ctx,
}: FinalizeHostEmailProps) => {
return (
<EmailLayout ctx={ctx} preview="Final date booked!">
<Heading>Final date booked!</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("finalizeHost_preview", {
ns: "emails",
defaultValue:
"Final date booked! We've notified participants and sent them calendar invites.",
title,
})}
>
<Heading>
{ctx.t("finalizeHost_heading", {
defaultValue: "Final date booked!",
ns: "emails",
})}
</Heading>
<Text>
<strong>{title}</strong> has been booked for:
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="finalizeHost_content"
ns="emails"
values={{ title }}
components={{
b: <strong />,
}}
defaults="<b>{{title}}</b> has been booked for:"
/>
</Text>
<Section>
<Row>
@ -76,13 +100,33 @@ export const FinalizeHostEmail = ({
</Row>
</Section>
<Text>
We&apos;ve notified participants and sent them calendar invites.
{ctx.t("finalizeHost_content2", {
defaultValue:
"We've notified participants and sent them calendar invites.",
ns: "emails",
})}
</Text>
<Section style={{ marginTop: 32 }}>
<Button href={pollUrl}>View Event</Button>
<Button href={pollUrl}>
{ctx.t("finalizeHost_button", {
defaultValue: "View Event",
ns: "emails",
})}
</Button>
</Section>
</EmailLayout>
);
};
export default FinalizeHostEmail;
FinalizeHostEmail.getSubject = (
props: FinalizeHostEmailProps,
ctx: EmailContext,
) => {
return ctx.t("finalizeHost_subject", {
defaultValue: "Date booked for {{title}}",
title: props.title,
ns: "emails",
});
};
export { FinalizeHostEmail };

View file

@ -1,44 +1,62 @@
import { Column, Row, Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
borderColor,
Button,
Heading,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
export interface FinalizeParticipantEmailProps {
date: string;
day: string;
dow: string;
time: string;
name: string;
title: string;
hostName: string;
location: string | null;
pollUrl: string;
attendees: string[];
ctx: EmailContext;
}
export const FinalizeParticipantEmail = ({
title = "Untitled Poll",
hostName = "Host",
pollUrl = "https://rallly.co",
day = "12",
dow = "Fri",
date = "Friday, 12th June 2020",
time = "6:00 PM to 11:00 PM BST",
ctx = defaultEmailContext,
const FinalizeParticipantEmail = ({
title,
hostName,
pollUrl,
day,
dow,
date,
time,
ctx,
}: FinalizeParticipantEmailProps) => {
return (
<EmailLayout ctx={ctx} preview="Final date booked!">
<Heading>Final date booked!</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("finalizeParticipant_preview", {
defaultValue: "Final date booked!",
ns: "emails",
})}
>
<Heading>
{ctx.t("finalizeParticipant_heading", {
defaultValue: "Final date booked!",
ns: "emails",
})}
</Heading>
<Text>
<strong>{hostName}</strong> has booked <strong>{title}</strong> for the
following date:
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="finalizeParticipant_content"
ns="emails"
defaults="<b>{{hostName}}</b> has booked <b>{{title}}</b> for the following date:"
values={{ hostName, title }}
components={{
b: <strong />,
}}
/>
</Text>
<Section data-testid="date-section">
<Row>
@ -78,7 +96,12 @@ export const FinalizeParticipantEmail = ({
</Column>
</Row>
</Section>
<Text>Please find attached a calendar invite for this event.</Text>
<Text>
{ctx.t("finalizeParticipant_content2", {
defaultValue:
"Please find attached a calendar invite for this event.",
})}
</Text>
<Section style={{ marginTop: 32 }}>
<Button href={pollUrl}>View Event</Button>
</Section>
@ -86,4 +109,15 @@ export const FinalizeParticipantEmail = ({
);
};
export default FinalizeParticipantEmail;
FinalizeParticipantEmail.getSubject = (
props: FinalizeParticipantEmailProps,
ctx: EmailContext,
) => {
return ctx.t("finalizeParticipant_subject", {
defaultValue: "Date booked for {{title}}",
title: props.title,
ns: "emails",
});
};
export { FinalizeParticipantEmail };

View file

@ -1,32 +1,42 @@
import { Section } from "@react-email/components";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
Button,
Card,
Domain,
Heading,
Link,
Text,
trackingWide,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
interface LoginEmailProps {
name: string;
code: string;
magicLink: string;
ctx: EmailContext;
}
export const LoginEmail = ({
code = "123456",
magicLink = "https://rallly.co",
ctx = defaultEmailContext,
}: LoginEmailProps) => {
export const LoginEmail = ({ code, magicLink, ctx }: LoginEmailProps) => {
return (
<EmailLayout ctx={ctx} preview="Use this link to log in on this device.">
<Heading>Login</Heading>
<Text>Enter this one-time 6-digit verification code:</Text>
<EmailLayout
ctx={ctx}
preview={ctx.t("login_preview", {
defaultValue: "Use this link to log in on this device.",
ns: "emails",
})}
>
<Heading>
{ctx.t("login_heading", { defaultValue: "Login", ns: "emails" })}
</Heading>
<Text>
{ctx.t("login_content", {
defaultValue: "Enter this one-time 6-digit verification code:",
ns: "emails",
})}
</Text>
<Card style={{ textAlign: "center" }}>
<Text
style={{
@ -40,21 +50,48 @@ export const LoginEmail = ({
{code}
</Text>
<Text style={{ textAlign: "center" }} light={true}>
This code is valid for 15 minutes
{ctx.t("login_codeValid", {
defaultValue: "This code is valid for 15 minutes",
ns: "emails",
})}
</Text>
</Card>
<Section style={{ marginBottom: 32 }}>
<Button href={magicLink} id="magicLink">
Log in to {ctx.domain}
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="login_button"
defaults="Log in to {domain}"
values={{ domain: ctx.domain }}
ns="emails"
/>
</Button>
</Section>
<Text light>
You&apos;re receiving this email because a request was made to login to{" "}
<Domain ctx={ctx} />. If this wasn&apos;t you contact{" "}
<a href={`mailto:${ctx.supportEmail}`}>{ctx.supportEmail}</a>.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="login_content2"
defaults="You're receiving this email because a request was made to login to <domain />. If this wasn't you contact <a>{supportEmail}</a>."
values={{ supportEmail: ctx.supportEmail }}
components={{
domain: <Domain ctx={ctx} />,
a: <Link href={`mailto:${ctx.supportEmail}`} />,
}}
ns="emails"
/>
</Text>
</EmailLayout>
);
};
LoginEmail.getSubject = (props: LoginEmailProps, ctx: EmailContext) => {
return ctx.t("login_subject", {
defaultValue: "{{code}} is your 6-digit code",
code: props.code,
ns: "emails",
});
};
export default LoginEmail;

View file

@ -1,36 +1,70 @@
import { defaultEmailContext } from "./_components/email-context";
import { Trans } from "react-i18next/TransWithoutContext";
import NotificationEmail, {
NotificationBaseProps,
} from "./_components/notification-email";
import { Heading, Text } from "./_components/styled-components";
} from "../components/notification-email";
import { Heading, Text } from "../components/styled-components";
import type { EmailContext } from "../types";
export interface NewCommentEmailProps extends NotificationBaseProps {
authorName: string;
}
export const NewCommentEmail = ({
name = "Guest",
title = "Untitled Poll",
authorName = "Someone",
pollUrl = "https://rallly.co",
disableNotificationsUrl = "https://rallly.co",
ctx = defaultEmailContext,
const NewCommentEmail = ({
title,
authorName,
pollUrl,
disableNotificationsUrl,
ctx,
}: NewCommentEmailProps) => {
return (
<NotificationEmail
ctx={ctx}
name={name}
title={title}
pollUrl={pollUrl}
disableNotificationsUrl={disableNotificationsUrl}
preview="Go to your poll to see what they said."
preview={ctx.t("newComment_preview", {
ns: "emails",
defaultValue: "Go to your poll to see what they said.",
})}
>
<Heading>New Comment</Heading>
<Heading>
<Trans
i18n={ctx.i18n}
ns="emails"
i18nKey="newComment_heading"
defaults="New Comment"
/>
</Heading>
<Text>
<strong>{authorName}</strong> has commented on <strong>{title}</strong>.
<Trans
i18n={ctx.i18n}
ns="emails"
i18nKey="newComment_content"
defaults="<b>{{authorName}}</b> has commented on <b>{{title}}</b>."
components={{
b: <strong />,
}}
values={{
authorName,
title,
}}
/>
</Text>
</NotificationEmail>
);
};
export default NewCommentEmail;
NewCommentEmail.getSubject = (
props: NewCommentEmailProps,
ctx: EmailContext,
) => {
return ctx.t("newComment_subject", {
ns: "emails",
defaultValue: "{{authorName}} has commented on {{title}}",
authorName: props.authorName,
title: props.title,
});
};
export { NewCommentEmail };

View file

@ -1,46 +1,99 @@
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailLayout } from "../components/email-layout";
import {
Button,
Domain,
Heading,
Section,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
interface NewParticipantConfirmationEmailProps {
name: string;
title: string;
editSubmissionUrl: string;
ctx: EmailContext;
}
export const NewParticipantConfirmationEmail = ({
title = "Untitled Poll",
editSubmissionUrl = "https://rallly.co",
ctx = defaultEmailContext,
title,
editSubmissionUrl,
ctx,
}: NewParticipantConfirmationEmailProps) => {
const { domain } = ctx;
return (
<EmailLayout ctx={ctx} preview="To edit your response use the link below">
<Heading>Poll Response Confirmation</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("newParticipantConfirmation_preview", {
defaultValue: "To edit your response use the link below",
ns: "emails",
})}
>
<Heading>
{ctx.t("newParticipantConfirmation_heading", {
defaultValue: "Poll Response Confirmation",
ns: "emails",
})}
</Heading>
<Text>
Your response to <strong>{title}</strong> has been submitted.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_content"
defaults="Your response to <b>{{title}}</b> has been submitted."
components={{
b: <strong />,
}}
values={{ title }}
ns="emails"
/>
</Text>
<Text>
While the poll is still open you can change your response using the link
below.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_content2"
defaults="While the poll is still open you can change your response using the link below."
ns="emails"
/>
</Text>
<Section style={{ marginTop: 32 }}>
<Button id="editSubmissionUrl" href={editSubmissionUrl}>
Review response on {domain}
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_button"
defaults="Review response on {domain}"
values={{ domain }}
ns="emails"
/>
</Button>
</Section>
<Text light>
You are receiving this email because a response was submitted on{" "}
<Domain ctx={ctx} />. If this wasn&apos;t you, please ignore this email.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipantConfirmation_footnote"
defaults="You are receiving this email because a response was submitted on <domain />. If this wasn't you, please ignore this email."
components={{
domain: <Domain ctx={ctx} />,
}}
ns="emails"
/>
</Text>
</EmailLayout>
);
};
NewParticipantConfirmationEmail.getSubject = (
props: NewParticipantConfirmationEmailProps,
ctx: EmailContext,
) => {
return ctx.t("newParticipantConfirmation_subject", {
defaultValue: "Thanks for responding to {{title}}",
title: props.title,
ns: "emails",
});
};
export default NewParticipantConfirmationEmail;

View file

@ -1,38 +1,75 @@
import { defaultEmailContext } from "./_components/email-context";
import { Trans } from "react-i18next/TransWithoutContext";
import NotificationEmail, {
NotificationBaseProps,
} from "./_components/notification-email";
import { Heading, Text } from "./_components/styled-components";
} from "../components/notification-email";
import { Heading, Text } from "../components/styled-components";
import type { EmailContext } from "../types";
export interface NewParticipantEmailProps extends NotificationBaseProps {
participantName: string;
}
export const NewParticipantEmail = ({
name = "John",
title = "Untitled Poll",
participantName = "Someone",
pollUrl = "https://rallly.co",
disableNotificationsUrl = "https://rallly.co",
ctx = defaultEmailContext,
const NewParticipantEmail = ({
title,
participantName,
pollUrl,
disableNotificationsUrl,
ctx,
}: NewParticipantEmailProps) => {
return (
<NotificationEmail
ctx={ctx}
name={name}
title={title}
pollUrl={pollUrl}
disableNotificationsUrl={disableNotificationsUrl}
preview="Go to your poll to see the new response."
preview={ctx.t("newParticipant_preview", {
defaultValue: "Go to your poll to see the new response.",
ns: "emails",
})}
>
<Heading>New Response</Heading>
<Heading>
{ctx.t("newParticipant_heading", {
defaultValue: "New Response",
ns: "emails",
})}
</Heading>
<Text>
<strong>{participantName}</strong> has responded to{" "}
<strong>{title}</strong>.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipant_content"
ns="emails"
defaults="<b>{{name}}</b> has responded to <b>{{title}}</b>."
components={{
b: <strong />,
}}
values={{ name: participantName, title }}
/>
</Text>
<Text>
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newParticipant_content2"
defaults="Go to your poll to see the new response."
ns="emails"
/>
</Text>
<Text>Go to your poll to see the new response.</Text>
</NotificationEmail>
);
};
export default NewParticipantEmail;
NewParticipantEmail.getSubject = (
props: NewParticipantEmailProps,
ctx: EmailContext,
) => {
return ctx.t("newParticipant_subject", {
defaultValue: "{{name}} has responded to {{title}}",
name: props.participantName,
title: props.title,
ns: "emails",
});
};
export { NewParticipantEmail };

View file

@ -1,12 +1,14 @@
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { Trans } from "react-i18next/TransWithoutContext";
import { EmailLayout } from "../components/email-layout";
import {
Button,
Card,
Heading,
Link,
Text,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
export interface NewPollEmailProps {
title: string;
@ -17,29 +19,61 @@ export interface NewPollEmailProps {
}
export const NewPollEmail = ({
title = "Untitled Poll",
adminLink = "https://rallly.co/admin/abcdefg123",
participantLink = "https://rallly.co/invite/wxyz9876",
ctx = defaultEmailContext,
title,
adminLink,
participantLink,
ctx,
}: NewPollEmailProps) => {
return (
<EmailLayout
ctx={ctx}
preview="Share your participant link to start collecting responses."
preview={ctx.t("newPoll_preview", {
defaultValue:
"Share your participant link to start collecting responses.",
ns: "emails",
})}
>
<Heading>New Poll Created</Heading>
<Heading>
{ctx.t("newPoll_heading", {
defaultValue: "New Poll Created",
ns: "emails",
})}
</Heading>
<Text>
Your meeting poll titled <strong>{`"${title}"`}</strong> is ready! Share
it using the link below:
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="newPoll_content"
ns="emails"
values={{ title }}
components={{
b: <strong />,
}}
defaults="Your meeting poll titled <b>{{title}}</b> is ready! Share it using the link below:"
/>
</Text>
<Card style={{ textAlign: "center" }}>
<Text style={{ textAlign: "center" }}>
<Link href={participantLink}>{participantLink}</Link>
</Text>
</Card>
<Button href={adminLink}>Manage Poll &rarr;</Button>
<Button href={adminLink}>
{ctx.t("newPoll_button", {
defaultValue: "Manage Poll",
ns: "emails",
})}
&rarr;
</Button>
</EmailLayout>
);
};
NewPollEmail.getSubject = (props: NewPollEmailProps, ctx: EmailContext) => {
return ctx.t("newPoll_subject", {
defaultValue: "Let's find a date for {{title}}!",
title: props.title,
ns: "emails",
});
};
export default NewPollEmail;

View file

@ -1,29 +1,43 @@
import { Section } from "@react-email/section";
import { Trans } from "react-i18next/TransWithoutContext";
import { defaultEmailContext, EmailContext } from "./_components/email-context";
import { EmailLayout } from "./_components/email-layout";
import { EmailLayout } from "../components/email-layout";
import {
Card,
Domain,
Heading,
Text,
trackingWide,
} from "./_components/styled-components";
} from "../components/styled-components";
import type { EmailContext } from "../types";
interface RegisterEmailProps {
code: string;
ctx: EmailContext;
}
export const RegisterEmail = ({
code = "123456",
ctx = defaultEmailContext,
}: RegisterEmailProps) => {
export const RegisterEmail = ({ code, ctx }: RegisterEmailProps) => {
return (
<EmailLayout ctx={ctx} preview={`Your 6-digit code is: ${code}`}>
<Heading>Verify your email address</Heading>
<EmailLayout
ctx={ctx}
preview={ctx.t("register_preview", {
ns: "emails",
defaultValue: "Your 6-digit code is: {{code}}",
code,
})}
>
<Heading>
{ctx.t("register_heading", {
defaultValue: "Verify your email address",
ns: "emails",
})}
</Heading>
<Text>
Please use the following 6-digit verification code to verify your email:
{ctx.t("register_text", {
defaultValue:
"Please use the following 6-digit verification code to verify your email",
ns: "emails",
})}
</Text>
<Card style={{ textAlign: "center" }}>
<Text
@ -38,18 +52,36 @@ export const RegisterEmail = ({
{code}
</Text>
<Text style={{ textAlign: "center" }} light={true}>
This code is valid for 15 minutes
{ctx.t("register_codeValid", {
defaultValue: "This code is valid for 15 minutes",
ns: "emails",
})}
</Text>
</Card>
<Section>
<Text light={true}>
You&apos;re receiving this email because a request was made to
register an account on <Domain ctx={ctx} />. If this wasn&apos;t you,
please ignore this email.
<Trans
i18n={ctx.i18n}
t={ctx.t}
i18nKey="register_footer"
ns="emails"
values={{ domain: ctx.domain }}
components={{
domain: <Domain ctx={ctx} />,
}}
defaults="You're receiving this email because a request was made to register an account on <domain />. If this wasn't you, please ignore this email."
/>
</Text>
</Section>
</EmailLayout>
);
};
RegisterEmail.getSubject = (_props: RegisterEmailProps, ctx: EmailContext) => {
return ctx.t("register_subject", {
defaultValue: "Please verify your email address",
ns: "emails",
});
};
export default RegisterEmail;

View file

@ -0,0 +1,12 @@
import type { TFunction } from "i18next";
import type { I18nInstance } from "./i18n";
export type EmailContext = {
logoUrl: string;
baseUrl: string;
domain: string;
supportEmail: string;
i18n: I18nInstance;
t: TFunction;
};

View file

@ -1,5 +1,11 @@
{
"extends": "@rallly/tsconfig/react.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
"extends": "@rallly/tsconfig/next.json",
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"resolveJsonModule": true,
},
"files": ["i18next.d.ts"],
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", ".react-email"],
}

View file

@ -1 +1 @@
export { cn } from "@rallly/ui";
export { cn } from "./lib/utils";

View file

@ -107,6 +107,7 @@
"STRIPE_YEARLY_PRICE",
"SENTRY_ORG",
"SENTRY_PROJECT",
"SUPPORT_EMAIL"
"SUPPORT_EMAIL",
"REACT_EMAIL_LANG"
]
}

677
yarn.lock
View file

@ -2471,20 +2471,6 @@
resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323"
integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==
"@opentelemetry/api-logs@0.51.1":
version "0.51.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.51.1.tgz#ded1874c04516c2b8cb24828eef3d6c3d1f75343"
integrity sha512-E3skn949Pk1z2XtXu/lxf6QAZpawuTM/IUEXcAzpiUkTd73Hmvw26FiN3cJuTmkpM5hZzHwkomVdtrh/n/zzwA==
dependencies:
"@opentelemetry/api" "^1.0.0"
"@opentelemetry/api-logs@0.52.0":
version "0.52.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.0.tgz#b117c1fc6fc457249739bbe21571cefc55e5092c"
integrity sha512-HxjD7xH9iAE4OyhNaaSec65i1H6QZYBWSwWkowFfsc5YAcDvJG30/J1sRKXEQqdmUcKTXEAnA66UciqZha/4+Q==
dependencies:
"@opentelemetry/api" "^1.0.0"
"@opentelemetry/api-logs@0.52.1":
version "0.52.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc"
@ -2492,40 +2478,23 @@
dependencies:
"@opentelemetry/api" "^1.0.0"
"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.6.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0":
"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
"@opentelemetry/context-async-hooks@^1.25.0", "@opentelemetry/context-async-hooks@^1.25.1":
"@opentelemetry/context-async-hooks@^1.25.1":
version "1.25.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz#810bff2fcab84ec51f4684aff2d21f6c057d9e73"
integrity sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==
"@opentelemetry/core@1.25.0":
version "1.25.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.0.tgz#ad034f5c2669f589bd703bfbbaa38b51f8504053"
integrity sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==
dependencies:
"@opentelemetry/semantic-conventions" "1.25.0"
"@opentelemetry/core@1.25.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0":
"@opentelemetry/core@1.25.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0":
version "1.25.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.1.tgz#ff667d939d128adfc7c793edae2f6bca177f829d"
integrity sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==
dependencies:
"@opentelemetry/semantic-conventions" "1.25.1"
"@opentelemetry/instrumentation-connect@0.37.0":
version "0.37.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.37.0.tgz#ab1bc3d33058bfc647d4b158295b589d11d619df"
integrity sha512-SeQktDIH5rNzjiEiazWiJAIXkmnLOnNV7wwHpahrqE0Ph+Z3heqMfxRtoMtbdJSIYLfcNZYO51AjxZ00IXufdw==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@types/connect" "3.4.36"
"@opentelemetry/instrumentation-connect@0.38.0":
version "0.38.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.38.0.tgz#1f4aa27894eac2538fb3c8fce7b1be92cae0217e"
@ -2536,15 +2505,6 @@
"@opentelemetry/semantic-conventions" "^1.22.0"
"@types/connect" "3.4.36"
"@opentelemetry/instrumentation-express@0.40.1":
version "0.40.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.40.1.tgz#b4c31a352691b060b330e4c028a8ef5472b89e27"
integrity sha512-+RKMvVe2zw3kIXRup9c1jFu3T4d0fs5aKy015TpiMyoCKX1UMu3Z0lfgYtuyiSTANvg5hZnDbWmQmqSPj9VTvg==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-express@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz#658561df6ffbae86f5ad33e8d7ef2abb7b4967fc"
@ -2554,15 +2514,6 @@
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-fastify@0.37.0":
version "0.37.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.37.0.tgz#c9537050d222d89ad4c3930b7b21a58016206f6d"
integrity sha512-WRjwzNZgupSzbEYvo9s+QuHJRqZJjVdNxSEpGBwWK8RKLlHGwGVAu0gcc2gPamJWUJsGqPGvahAPWM18ZkWj6A==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-fastify@0.38.0":
version "0.38.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.38.0.tgz#0cb02ee1156197075e8a90e4fd18a6b6c94221ba"
@ -2572,11 +2523,12 @@
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-graphql@0.41.0":
version "0.41.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.41.0.tgz#b3f1c7e0bb18400b1336f781f209f6b73608bd89"
integrity sha512-R/gXeljgIhaRDKquVkKYT5QHPnFouM8ooyePZEP0kqyaVAedtR1V7NfAUJbxfTG5fBQa5wdmLjvu63+tzRXZCA==
"@opentelemetry/instrumentation-fs@0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.14.0.tgz#19f1cb38a8c2d05f3b96af67f1c8d43f0af2829b"
integrity sha512-pVc8P5AgliC1DphyyBUgsxXlm2XaPH4BpYvt7rAZDMIqUpRk8gs19SioABtKqqxvFzg5jPtgJfJsdxq0Y+maLw==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/instrumentation-graphql@0.42.0":
@ -2586,15 +2538,6 @@
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/instrumentation-hapi@0.39.0":
version "0.39.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.39.0.tgz#c6a43440baac714aba57d12ee363b72a02378eed"
integrity sha512-ik2nA9Yj2s2ay+aNY+tJsKCsEx6Tsc2g/MK0iWBW5tibwrWKTy1pdVt5sB3kd5Gkimqj23UV5+FH2JFcQLeKug==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-hapi@0.40.0":
version "0.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.40.0.tgz#ae11190f0f57cdb4dc8d792cb8bca61e5343684c"
@ -2604,16 +2547,6 @@
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-http@0.52.0":
version "0.52.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.0.tgz#a2fd280a493591d2cf4db534253ca406580569f7"
integrity sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA==
dependencies:
"@opentelemetry/core" "1.25.0"
"@opentelemetry/instrumentation" "0.52.0"
"@opentelemetry/semantic-conventions" "1.25.0"
semver "^7.5.2"
"@opentelemetry/instrumentation-http@0.52.1":
version "0.52.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz#12061501601838d1c912f9c29bdd40a13a7e44cf"
@ -2624,15 +2557,6 @@
"@opentelemetry/semantic-conventions" "1.25.1"
semver "^7.5.2"
"@opentelemetry/instrumentation-ioredis@0.41.0":
version "0.41.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.41.0.tgz#41b60babdce893df7466b13a8896a71c81a80813"
integrity sha512-rxiLloU8VyeJGm5j2fZS8ShVdB82n7VNP8wTwfUQqDwRfHCnkzGr+buKoxuhGD91gtwJ91RHkjHA1Eg6RqsUTg==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/redis-common" "^0.36.2"
"@opentelemetry/semantic-conventions" "^1.23.0"
"@opentelemetry/instrumentation-ioredis@0.42.0":
version "0.42.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz#0f488ffc68af3caa474e2f67861759075170729c"
@ -2642,17 +2566,6 @@
"@opentelemetry/redis-common" "^0.36.2"
"@opentelemetry/semantic-conventions" "^1.23.0"
"@opentelemetry/instrumentation-koa@0.41.0":
version "0.41.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.41.0.tgz#31d75ebc4c53c9c902f7ef3f73e52d575fce9628"
integrity sha512-mbPnDt7ELvpM2S0vixYUsde7122lgegLOJQxx8iJQbB8YHal/xnTh9v7IfArSVzIDo+E+080hxZyUZD4boOWkw==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@types/koa" "2.14.0"
"@types/koa__router" "12.0.3"
"@opentelemetry/instrumentation-koa@0.42.0":
version "0.42.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.42.0.tgz#1c180f3605448c2e57a4ba073b69ffba7b2970b3"
@ -2662,15 +2575,6 @@
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-mongodb@0.45.0":
version "0.45.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.45.0.tgz#d6373e30f3e83eba87f7e6e2ea72c1351467d6b5"
integrity sha512-xnZP9+ayeB1JJyNE9cIiwhOJTzNEsRhXVdLgfzmrs48Chhhk026mQdM5CITfyXSCfN73FGAIB8d91+pflJEfWQ==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/sdk-metrics" "^1.9.1"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-mongodb@0.46.0":
version "0.46.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.46.0.tgz#e3720e8ca3ca9f228fbf02f0812f7518c030b05e"
@ -2680,15 +2584,6 @@
"@opentelemetry/sdk-metrics" "^1.9.1"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-mongoose@0.39.0":
version "0.39.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.39.0.tgz#2d5070bb0838769b8dd099b6402f42e1269f527a"
integrity sha512-J1r66A7zJklPPhMtrFOO7/Ud2p0Pv5u8+r23Cd1JUH6fYPmftNJVsLp2urAt6PHK4jVqpP/YegN8wzjJ2mZNPQ==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-mongoose@0.40.0":
version "0.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.40.0.tgz#9c888312e524c381bfdf56a094c799150332dd51"
@ -2698,15 +2593,6 @@
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-mysql2@0.39.0":
version "0.39.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.39.0.tgz#1719441f58e3f3418c2c3a7b15b48c187d8e3f90"
integrity sha512-Iypuq2z6TCfriAXCIZjRq8GTFCKhQv5SpXbmI+e60rYdXw8NHtMH4NXcGF0eKTuoCsC59IYSTUvDQYDKReaszA==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/sql-common" "^0.40.1"
"@opentelemetry/instrumentation-mysql2@0.40.0":
version "0.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.40.0.tgz#fa2992c36d54427dccea68e5c69fff01103dabe6"
@ -2716,15 +2602,6 @@
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/sql-common" "^0.40.1"
"@opentelemetry/instrumentation-mysql@0.39.0":
version "0.39.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.39.0.tgz#b55afe5b1249363f42c6092529466b057297ab94"
integrity sha512-8snHPh83rhrDf31v9Kq0Nf+ts8hdr7NguuszRqZomZBHgE0+UyXZSkXHAAFZoBPPRMGyM68uaFE5hVtFl+wOcA==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@types/mysql" "2.15.22"
"@opentelemetry/instrumentation-mysql@0.40.0":
version "0.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.40.0.tgz#bde5894c8eb447a4b8e940b030b2b73898da03fa"
@ -2734,14 +2611,6 @@
"@opentelemetry/semantic-conventions" "^1.22.0"
"@types/mysql" "2.15.22"
"@opentelemetry/instrumentation-nestjs-core@0.38.0":
version "0.38.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.38.0.tgz#d4296936723f1dfbd11747a84a87d17a3da0bc74"
integrity sha512-M381Df1dM8aqihZz2yK+ugvMFK5vlHG/835dc67Sx2hH4pQEQYDA2PpFPTgc9AYYOydQaj7ClFQunESimjXDgg==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.23.0"
"@opentelemetry/instrumentation-nestjs-core@0.39.0":
version "0.39.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.39.0.tgz#733fef4306c796951d7ea1951b45f9df0aed234d"
@ -2750,17 +2619,6 @@
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.23.0"
"@opentelemetry/instrumentation-pg@0.42.0":
version "0.42.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.42.0.tgz#a73de6c057b4a8b99c964d2bbf2fdad304284be9"
integrity sha512-sjgcM8CswYy8zxHgXv4RAZ09DlYhQ+9TdlourUs63Df/ek5RrB1ZbjznqW7PB6c3TyJJmX6AVtPTjAsROovEjA==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/sql-common" "^0.40.1"
"@types/pg" "8.6.1"
"@types/pg-pool" "2.0.4"
"@opentelemetry/instrumentation-pg@0.43.0":
version "0.43.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.43.0.tgz#3cd94ad5144e1fd326a921280fa8bb7b49005eb5"
@ -2772,15 +2630,6 @@
"@types/pg" "8.6.1"
"@types/pg-pool" "2.0.4"
"@opentelemetry/instrumentation-redis-4@0.40.0":
version "0.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.40.0.tgz#4a1bc9bebfb869de8d982b1a1a5b550bdb68d15b"
integrity sha512-0ieQYJb6yl35kXA75LQUPhHtGjtQU9L85KlWa7d4ohBbk/iQKZ3X3CFl5jC5vNMq/GGPB3+w3IxNvALlHtrp7A==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/redis-common" "^0.36.2"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation-redis-4@0.41.0":
version "0.41.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.0.tgz#6c1b1a37c18478887f346a3bc7ef309ee9f726c0"
@ -2790,18 +2639,6 @@
"@opentelemetry/redis-common" "^0.36.2"
"@opentelemetry/semantic-conventions" "^1.22.0"
"@opentelemetry/instrumentation@0.52.0":
version "0.52.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz#f8b790bfb1c61c27e0ba846bc6d0e377da195d1e"
integrity sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ==
dependencies:
"@opentelemetry/api-logs" "0.52.0"
"@types/shimmer" "^1.0.2"
import-in-the-middle "1.8.0"
require-in-the-middle "^7.1.1"
semver "^7.5.2"
shimmer "^1.2.1"
"@opentelemetry/instrumentation@0.52.1", "@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.0", "@opentelemetry/instrumentation@^0.52.1":
version "0.52.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48"
@ -2814,17 +2651,6 @@
semver "^7.5.2"
shimmer "^1.2.1"
"@opentelemetry/instrumentation@^0.43.0":
version "0.43.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b"
integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ==
dependencies:
"@types/shimmer" "^1.0.2"
import-in-the-middle "1.4.2"
require-in-the-middle "^7.1.1"
semver "^7.5.2"
shimmer "^1.2.1"
"@opentelemetry/instrumentation@^0.46.0":
version "0.46.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.46.0.tgz#a8a252306f82e2eace489312798592a14eb9830e"
@ -2836,24 +2662,12 @@
semver "^7.5.2"
shimmer "^1.2.1"
"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51":
version "0.51.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.51.1.tgz#46fb2291150ec6923e50b2f094b9407bc726ca9b"
integrity sha512-JIrvhpgqY6437QIqToyozrUG1h5UhwHkaGK/WAX+fkrpyPtc+RO5FkRtUd9BH0MibabHHvqsnBGKfKVijbmp8w==
dependencies:
"@opentelemetry/api-logs" "0.51.1"
"@types/shimmer" "^1.0.2"
import-in-the-middle "1.7.4"
require-in-the-middle "^7.1.1"
semver "^7.5.2"
shimmer "^1.2.1"
"@opentelemetry/redis-common@^0.36.2":
version "0.36.2"
resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz#906ac8e4d804d4109f3ebd5c224ac988276fdc47"
integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==
"@opentelemetry/resources@1.25.1", "@opentelemetry/resources@^1.25.0", "@opentelemetry/resources@^1.25.1":
"@opentelemetry/resources@1.25.1", "@opentelemetry/resources@^1.25.1":
version "1.25.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.25.1.tgz#bb9a674af25a1a6c30840b755bc69da2796fefbb"
integrity sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==
@ -2870,7 +2684,7 @@
"@opentelemetry/resources" "1.25.1"
lodash.merge "^4.6.2"
"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.25.0", "@opentelemetry/sdk-trace-base@^1.25.1":
"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.25.1":
version "1.25.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz#cbc1e60af255655d2020aa14cde17b37bd13df37"
integrity sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==
@ -2879,12 +2693,7 @@
"@opentelemetry/resources" "1.25.1"
"@opentelemetry/semantic-conventions" "1.25.1"
"@opentelemetry/semantic-conventions@1.25.0":
version "1.25.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz#390eb4d42a29c66bdc30066af9035645e9bb7270"
integrity sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==
"@opentelemetry/semantic-conventions@1.25.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.23.0", "@opentelemetry/semantic-conventions@^1.25.0", "@opentelemetry/semantic-conventions@^1.25.1":
"@opentelemetry/semantic-conventions@1.25.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.23.0", "@opentelemetry/semantic-conventions@^1.25.1":
version "1.25.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e"
integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==
@ -2991,19 +2800,10 @@
dependencies:
"@prisma/debug" "5.17.0"
"@prisma/instrumentation@5.15.0":
version "5.15.0"
resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.15.0.tgz#9ec061b35761579ffa896bdf19c6a0bf53247593"
integrity sha512-fCWOOOajTKOUEp43gRmBqwt6oN9bPJcLiloi2OG/2ED0N5z62Cuza6FDrlm3SJHQAXYlXqLE0HLdEE5WcUkOzg==
dependencies:
"@opentelemetry/api" "^1.8"
"@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51"
"@opentelemetry/sdk-trace-base" "^1.22"
"@prisma/instrumentation@5.17.0":
version "5.17.0"
resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.17.0.tgz#f741ff517f54b1a896fb8605e0d702f29855c6cb"
integrity sha512-c1Sle4ji8aasMcYfBBHFM56We4ljfenVtRmS8aY06BllS7SoU6SmJBwG7vil+GHiR0Yrh+t9iBwt4AY0Jr4KNQ==
"@prisma/instrumentation@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.18.0.tgz#8b49e25bf3f8f756eb0c4c199b4cf8b6631db891"
integrity sha512-r074avGkpPXItk+josQPhufZEmGhUCb16PQx4ITPS40vWTpTPET4VsgCBZB2alIN6SS7pRFod2vz2M2HHEEylQ==
dependencies:
"@opentelemetry/api" "^1.8"
"@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0"
@ -4220,131 +4020,61 @@
domhandler "^5.0.3"
selderee "^0.11.0"
"@sentry-internal/browser-utils@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.11.0.tgz#ee73594bde569608b0dd0c8aa499f8497dbb0f53"
integrity sha512-PCnmzeLm7eTdMleVWa1jbdNcB6M5R17CSX8oQF6A/5a2w9qW6HbjEwK6X4yc9MzsFXFaTNekvPQLMRhIE1MgpA==
"@sentry-internal/browser-utils@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.27.0.tgz#b8fd1c5e6b8c01d76abbba7ac5017eebcf7d3ed2"
integrity sha512-YTIwQ1GM1NTRXgN4DvpFSQ2x4pjlqQ0FQAyHW5x2ZYv4z7VmqG4Xkid1P/srQUipECk6nxkebfD4WR19nLsvnQ==
dependencies:
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry-internal/browser-utils@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.22.0.tgz#29f35a617ec69d881141decdcdbbe3a777528392"
integrity sha512-R0u8KPaSivueIwUOhmYxcisKaJq3gx+I0xOcWoluDB3OI1Ds/QOSP/vmTsMg/mjwG/nUJ8RRM8pj0s8vlqCrjg==
"@sentry-internal/feedback@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.27.0.tgz#46a4cbde49d7a1cb182792c28341a8c89249e6b1"
integrity sha512-b71PQc9aK1X9b/SO1DiJlrnAEx4n0MzPZQ/tKd9oRWDyGit6pJWZfQns9r2rvc96kJPMOTxFAa/upXRCkA723A==
dependencies:
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry-internal/feedback@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.11.0.tgz#72bfa11678dfa19857a3cf6943a05f0ddb72f12d"
integrity sha512-cMiFAuHP4jXCqWD7/UA5cvl0ee3hN5klAWTDVCZutnZ30pbUurg+nIggYBcaxdtLlqW6BCwyVs2nb/OB75CCSQ==
"@sentry-internal/replay-canvas@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.27.0.tgz#24a154f37b200ed99bb99a39cf98c35f25c2b93b"
integrity sha512-uuEfiWbjwugB9M4KxXxovHYiKRqg/R6U4EF8xM/Ub4laUuEcWsfRp7lQ3MxL3qYojbca8ncIFic2bIoKMPeejA==
dependencies:
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry-internal/replay" "8.27.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry-internal/feedback@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.22.0.tgz#d6b222ea08d72886b3efbc0760c8cf1f71b3e7fc"
integrity sha512-Sy2+v0xBmVnZ5LQ48603CvLy5vVQvAZ+hc9xQSAHexts07NkvApMU1qv26YNwxlAWfDha1wXiW6ryd4YDzaoVA==
"@sentry-internal/replay@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.27.0.tgz#7762647930c3a9b3d99f6d4c486b28f9d3da70c2"
integrity sha512-Ofucncaon98dvlxte2L//hwuG9yILSxNrTz/PmO0k+HzB9q+oBic4667QF+azWR2qv4oKSWpc+vEovP3hVqveA==
dependencies:
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry-internal/replay-canvas@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.11.0.tgz#c01c1385c426a39189bec151a74bc173b6068a4b"
integrity sha512-SrBFI0vwyeyUjibCbYfxzCNMd07QMDNoi+0SYzhBagp6ALbU8r/pa02JRsnr//qhZt2NOM6S2kks9e2VHr6hYg==
dependencies:
"@sentry-internal/replay" "8.11.0"
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry-internal/replay-canvas@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.22.0.tgz#70c5951a9d54f2e814e930fbe0371ab83fee1e72"
integrity sha512-/gV8qN3JqWw0LXTMuCGB8RDI8Bx1VESNRBdh/7Cmc5+hxYBfcketuix3S8mHWcE/JO+Ed9g1Abzys6GphTB9LA==
dependencies:
"@sentry-internal/replay" "8.22.0"
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry-internal/replay@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.11.0.tgz#58c923fbae32204ad32e1e99c01c4040c1e06b2f"
integrity sha512-NyuHW1Ds2GGW6PjN7nnRl/XoM31Y/BUnOhhLbNmbxWj5mgWuUP/7tOlz2PhP0YqZxVteZ99QIssfSRgtYOeQlg==
dependencies:
"@sentry-internal/browser-utils" "8.11.0"
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry-internal/replay@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.22.0.tgz#c76bbe4575f8ab478694fe54d4bae87010315d3a"
integrity sha512-sF8RyMPJP1fSIyyBDAbtybvKCu0dy8ZAfMwLP7ZqEnWrhZqktVuqM7/++EAFMlD5YaWJXm1IDuOXjgSQjUtSIQ==
dependencies:
"@sentry-internal/browser-utils" "8.22.0"
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/babel-plugin-component-annotate@2.18.0":
version "2.18.0"
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.18.0.tgz#3bee98f94945643b0762ceed1f6cca60db52bdbd"
integrity sha512-9L4RbhS3WNtc/SokIhc0dwgcvs78YSQPakZejsrIgnzLzCi8mS6PeT+BY0+QCtsXxjd1egM8hqcJeB0lukBkXA==
"@sentry-internal/browser-utils" "8.27.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry/babel-plugin-component-annotate@2.20.1":
version "2.20.1"
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.20.1.tgz#204c63ed006a048f48f633876e1b8bacf87a9722"
integrity sha512-4mhEwYTK00bIb5Y9UWIELVUfru587Vaeg0DQGswv4aIRHIiMKLyNqCEejaaybQ/fNChIZOKmvyqXk430YVd7Qg==
"@sentry/browser@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.11.0.tgz#9e0282a9136d138c8b6c001f0ac9face9a9ce18b"
integrity sha512-++5IrBpzkaAptNjAYnGTnQ2lCjmU6nlu/ABFjUTgi7Vu+ZNiY8qYKEUw65mSxD3EoFLt8IwtjvfAwSMVTB2q8w==
"@sentry/browser@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.27.0.tgz#997eb6b3c298a659a109704a0fb660eae365cd3a"
integrity sha512-eL1eaHwoYUGkp4mpeYesH6WtCrm+0u9jYCW5Lm0MAeTmpx22BZKEmj0OljuUJXGnJwFbvPDlRjyz6QG11m8kZA==
dependencies:
"@sentry-internal/browser-utils" "8.11.0"
"@sentry-internal/feedback" "8.11.0"
"@sentry-internal/replay" "8.11.0"
"@sentry-internal/replay-canvas" "8.11.0"
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/browser@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.22.0.tgz#ee15b0ef7985732d56d01b62adbbc7b76bcc7ddc"
integrity sha512-t3b+/9WWcP9SQTWwrHrB57B33ENgmUjyFlW2+JSlCXkSJBSmAoquPZ/GPjOuPaSr3HIA0mu9uEr4A41d5diASQ==
dependencies:
"@sentry-internal/browser-utils" "8.22.0"
"@sentry-internal/feedback" "8.22.0"
"@sentry-internal/replay" "8.22.0"
"@sentry-internal/replay-canvas" "8.22.0"
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/bundler-plugin-core@2.18.0":
version "2.18.0"
resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.18.0.tgz#2411cd934e9510c53a2e682497a74172485ac817"
integrity sha512-JvxVgsMFmDsU0Dgcx1CeFUC1scxOVSAOzOcE06qKAVm9BZzxHpI53iNfeMOXwVTUolD8LZVIfgOjkiXfwN/UPQ==
dependencies:
"@babel/core" "^7.18.5"
"@sentry/babel-plugin-component-annotate" "2.18.0"
"@sentry/cli" "^2.22.3"
dotenv "^16.3.1"
find-up "^5.0.0"
glob "^9.3.2"
magic-string "0.30.8"
unplugin "1.0.1"
"@sentry-internal/browser-utils" "8.27.0"
"@sentry-internal/feedback" "8.27.0"
"@sentry-internal/replay" "8.27.0"
"@sentry-internal/replay-canvas" "8.27.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry/bundler-plugin-core@2.20.1":
version "2.20.1"
@ -4414,102 +4144,39 @@
"@sentry/cli-win32-i686" "2.32.1"
"@sentry/cli-win32-x64" "2.32.1"
"@sentry/core@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.11.0.tgz#bd78d9b598b6658ad18c8e0457fdffcd5f95b51e"
integrity sha512-rZaM55j5Fw0IGb8lNXOTVoq7WR6JmUzm9x5cURGsjL9gzAurGl817oK3iyOvYQ3JZnfijjh0QF0SQr4NZHKbIg==
"@sentry/core@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.27.0.tgz#a0ebe31cdd9313186a14d9738238ed9cf7a59c01"
integrity sha512-4frlXluHT3Du+Omw91K04jpvbfMtydvg4Bxj2+gt/DT19Swhm/fbEpzdUjgbAd3Jinj/n0qk/jFRXjr9JZKFjg==
dependencies:
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry/core@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.22.0.tgz#755387b85c1f7d849fb80acd7e8d54ee73ee2853"
integrity sha512-fYPnxp7UkY2tckaOtivIySxnJvlbekuxs+Qi6rkUv9JpF+TYKpt7OPNUAbgVIhS0xazAEN6iKTfmnmpUbFRLmQ==
dependencies:
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/nextjs@^8":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-8.22.0.tgz#9d6c3db699e3fffeeff3c65a911b9de46b9e21ce"
integrity sha512-XYb/3ocQLhZmdqqTgI7xce7AiRpHn3L6Sj3RVTBwNb4nb+XOfQ8o0LKF7v7yo6LGoQin+IWpWPACnNc8zH7fBA==
"@sentry/nextjs@*", "@sentry/nextjs@^8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-8.27.0.tgz#c6fece7bf856777e3b4b9d67db67af84df68a29c"
integrity sha512-fJgyBZj+arrNDtmxyKlWBm9ApxyzU3ydZPviSK3ub9gJemk0YqW/IKjF3PojtqLvtBnT81heDb/cysBadb+WpA==
dependencies:
"@opentelemetry/instrumentation-http" "0.52.1"
"@opentelemetry/semantic-conventions" "^1.25.1"
"@rollup/plugin-commonjs" "26.0.1"
"@sentry/core" "8.22.0"
"@sentry/node" "8.22.0"
"@sentry/opentelemetry" "8.22.0"
"@sentry/react" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/vercel-edge" "8.22.0"
"@sentry/core" "8.27.0"
"@sentry/node" "8.27.0"
"@sentry/opentelemetry" "8.27.0"
"@sentry/react" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry/vercel-edge" "8.27.0"
"@sentry/webpack-plugin" "2.20.1"
chalk "3.0.0"
resolve "1.22.8"
rollup "3.29.4"
stacktrace-parser "^0.1.10"
"@sentry/nextjs@^8.10.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-8.11.0.tgz#e5d9493ec4579aee70f459cb1d5c3ab8bf4e3749"
integrity sha512-PjmS9eLLJdzAqWnYDRxehmf8AEo8H6WB971x0gQb7j7n3ph/vNRy2pOGP895sBHbVDjLyRFwglr+akNQsbn8jQ==
dependencies:
"@opentelemetry/instrumentation-http" "0.52.0"
"@rollup/plugin-commonjs" "26.0.1"
"@sentry/core" "8.11.0"
"@sentry/node" "8.11.0"
"@sentry/opentelemetry" "8.11.0"
"@sentry/react" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/vercel-edge" "8.11.0"
"@sentry/webpack-plugin" "2.18.0"
chalk "3.0.0"
resolve "1.22.8"
rollup "3.29.4"
stacktrace-parser "^0.1.10"
"@sentry/node@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.11.0.tgz#773a25cf9f10dda7f39357bc9a94294aa301150c"
integrity sha512-hvPa2aUz1NAJ5AEcXVbll7ZM0LFgfYCvNBn6ZWb7d+segv/vbGwusTT1Xs7OlbF0UrNAx6rX5BJSAjQXQ2dlHg==
dependencies:
"@opentelemetry/api" "^1.9.0"
"@opentelemetry/context-async-hooks" "^1.25.0"
"@opentelemetry/core" "^1.25.0"
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/instrumentation-connect" "0.37.0"
"@opentelemetry/instrumentation-express" "0.40.1"
"@opentelemetry/instrumentation-fastify" "0.37.0"
"@opentelemetry/instrumentation-graphql" "0.41.0"
"@opentelemetry/instrumentation-hapi" "0.39.0"
"@opentelemetry/instrumentation-http" "0.52.0"
"@opentelemetry/instrumentation-ioredis" "0.41.0"
"@opentelemetry/instrumentation-koa" "0.41.0"
"@opentelemetry/instrumentation-mongodb" "0.45.0"
"@opentelemetry/instrumentation-mongoose" "0.39.0"
"@opentelemetry/instrumentation-mysql" "0.39.0"
"@opentelemetry/instrumentation-mysql2" "0.39.0"
"@opentelemetry/instrumentation-nestjs-core" "0.38.0"
"@opentelemetry/instrumentation-pg" "0.42.0"
"@opentelemetry/instrumentation-redis-4" "0.40.0"
"@opentelemetry/resources" "^1.25.0"
"@opentelemetry/sdk-trace-base" "^1.25.0"
"@opentelemetry/semantic-conventions" "^1.25.0"
"@prisma/instrumentation" "5.15.0"
"@sentry/core" "8.11.0"
"@sentry/opentelemetry" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
optionalDependencies:
opentelemetry-instrumentation-fetch-node "1.2.0"
"@sentry/node@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.22.0.tgz#588a01fd6746219d47eef8f7e4b73f7eebf6827e"
integrity sha512-xDLB4TJXT8iFKwoFneOIJtlfpsUB6L0m32Do97TrQ1dY2WilspksznrU2Ac+XfshdNxa8Sqr4tSj07Yzn0VNiQ==
"@sentry/node@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.27.0.tgz#91a56b907f4cf7f5d7b2cbb97dbee313383f2887"
integrity sha512-nE2VPSHOW/tzH/lB6CoBtYkmXqXncUuWMC56RLRiPyHEXDktZx8oFp364/3m117iKOjO0XHP57Kl5cdb90IM7g==
dependencies:
"@opentelemetry/api" "^1.9.0"
"@opentelemetry/context-async-hooks" "^1.25.1"
@ -4518,6 +4185,7 @@
"@opentelemetry/instrumentation-connect" "0.38.0"
"@opentelemetry/instrumentation-express" "0.41.1"
"@opentelemetry/instrumentation-fastify" "0.38.0"
"@opentelemetry/instrumentation-fs" "0.14.0"
"@opentelemetry/instrumentation-graphql" "0.42.0"
"@opentelemetry/instrumentation-hapi" "0.40.0"
"@opentelemetry/instrumentation-http" "0.52.1"
@ -4533,105 +4201,55 @@
"@opentelemetry/resources" "^1.25.1"
"@opentelemetry/sdk-trace-base" "^1.25.1"
"@opentelemetry/semantic-conventions" "^1.25.1"
"@prisma/instrumentation" "5.17.0"
"@sentry/core" "8.22.0"
"@sentry/opentelemetry" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@prisma/instrumentation" "5.18.0"
"@sentry/core" "8.27.0"
"@sentry/opentelemetry" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
import-in-the-middle "^1.11.0"
optionalDependencies:
opentelemetry-instrumentation-fetch-node "1.2.3"
"@sentry/opentelemetry@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.11.0.tgz#09d7f0aed6245b1fedddcad906c6d3a2255b7d87"
integrity sha512-OukaRoGNZbnCSN/neJGI72GwxpSZ0jg/44Rc8C//d8JKIFAZUn2in61k7fSbET0PSTKxw5erguvjcv/9BvLJOg==
"@sentry/opentelemetry@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.27.0.tgz#d5b55cf277ad1fc8111467887fd40ef428229a40"
integrity sha512-FRz7ApnyZYDFmi2CWUhKBux2N/0WswRLHwHDZ31FYCajujw7vQKucgdsxDW2RIRPWDwcMxHY1kvt6EzM1hIsxQ==
dependencies:
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry/opentelemetry@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.22.0.tgz#3589c548ddc14c30c72d7523d386c9705a92faeb"
integrity sha512-JqNsoyPdZ88Me2SdxAqq/5agcMzUzZ5xIjrM4ETC1aaeD+cPij/xL4U31b8S7aFJy3miaaZqFzpBy9A/YtFxLw==
"@sentry/react@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.27.0.tgz#3ff4b8ece65af170fe32102ecb8847b53c675ffd"
integrity sha512-8pD+J9UVnSGmPnm5dHJup5OVsHTN/pL4Ozi01yyrpivLkQiMZNac3OXsc0C7zXnztfLQx0kmTyCOzbRROfbpnA==
dependencies:
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/react@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.11.0.tgz#324353df35f6e9a28448fabd7501577be2fbcd7f"
integrity sha512-EyPOxDyRwOMPHRCc1/+dlWygXb6+92d0AbVTo4C8ZPT67aMWiczMzZC9qVUN6OqDVrpKwHMYzRyCdsu5OIIWHw==
dependencies:
"@sentry/browser" "8.11.0"
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/browser" "8.27.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
hoist-non-react-statics "^3.3.2"
"@sentry/react@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.22.0.tgz#6ad8c50d22262f14b83373ade0d77cf1c48adf70"
integrity sha512-LcO8SPfjYsx3Zvg1mQwjreVvtriVxde+6njIJyXU9TArB0e8bFexvd4MGXdBExgW9aY449hNaStgKRWMNHeVHQ==
"@sentry/types@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.27.0.tgz#a5c7d2877c6c3620f812b2b31377b58d390b89d4"
integrity sha512-B6lrP46+m2x0lfqWc9F4VcUbN893mVGnPEd7KIMRk95mPzkFJ3sNxggTQF5/ZfNO7lDQYQb22uysB5sj/BqFiw==
"@sentry/utils@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.27.0.tgz#308f6cc34acac175c500e4dd5b5007cdb621c79e"
integrity sha512-gyJM3SyLQe0A3mkQVVNdKYvk3ZoikkYgyA/D+5StFNLKdyUgEbJgXOGXrQSSYPF7BSX6Sc5b0KHCglPII0KuKw==
dependencies:
"@sentry/browser" "8.22.0"
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
hoist-non-react-statics "^3.3.2"
"@sentry/types" "8.27.0"
"@sentry/types@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.11.0.tgz#613da07f91d58ad5b460b95550e64aa6159ee97f"
integrity sha512-kz9/d2uw7wEXcK8DnCrCuMI75hZnpVAjYr8mq1uatltOx+2JOYPNdaK6ispxXlhb5KXOnVWNgfVDbGlLp0w+Gg==
"@sentry/types@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.22.0.tgz#98bfc8cebd37c93f5afe76c6df97d88116b32127"
integrity sha512-1MLK3xO+uF2oJaa+M98aLIrQsEHzV7xnVWPfE3MhejYLNQebj4rQnQKTut/xZNIF9W0Q+bRcakLarC3ce2a74g==
"@sentry/utils@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.11.0.tgz#b8856db2c1cfc1835df8854cba20cea82f6c6e08"
integrity sha512-iDt5YVMYNgT151bPYVGo8XlpM0MHWy8DH+czmAiAlFTV7ns7lAeHGF6tsFYo7wOZOPDHxtF6F2CM7AvuYnOZGw==
"@sentry/vercel-edge@8.27.0":
version "8.27.0"
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-8.27.0.tgz#ae2b91449c4676fff3c09e08b1feb5ef0069d9c9"
integrity sha512-ZBi8JHLQ1lUzw/nKMvGq1rFZTFkC3nhN4CeRLfFdTN3w3+76yejOnvSZZ6+Fl01kfdl6ThRnFdBvfNuXzjC9cQ==
dependencies:
"@sentry/types" "8.11.0"
"@sentry/utils@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.22.0.tgz#0e4411b19add83f84097fbf9c88423e69b8f6a38"
integrity sha512-0ITG2+3EtyMtyc/nQG8aB9z9eIQ4L43nM/KuNgYSnM1vPl/zegbaLT0Ek/xkQB1OLIOLkEPQ6x9GWe+248/n3g==
dependencies:
"@sentry/types" "8.22.0"
"@sentry/vercel-edge@8.11.0":
version "8.11.0"
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-8.11.0.tgz#661b2538e02ae7cdd73733e1f32730ce12137952"
integrity sha512-LdjTiuKaBH3s0LseBUlgyX4VW/b4t0QU3aCpIoU+nfoM71co9yaGhr9utI8OBQ4mkQA28jSzda6pDVlGk5+OXA==
dependencies:
"@sentry/core" "8.11.0"
"@sentry/types" "8.11.0"
"@sentry/utils" "8.11.0"
"@sentry/vercel-edge@8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-8.22.0.tgz#cb03d3b3eb55e8884b5caba99f1a0a2f8f23ddd2"
integrity sha512-2kkyJ+mvAOEIcM+YY21kMCjx2OI1r+4qYaKPwXWagns41+BcqAl1I8/lU6ZjbNP2wm9iy4kmYcZwx8P5VLtWAw==
dependencies:
"@sentry/core" "8.22.0"
"@sentry/types" "8.22.0"
"@sentry/utils" "8.22.0"
"@sentry/webpack-plugin@2.18.0":
version "2.18.0"
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.18.0.tgz#f0955d28b1271e9a8b6a2927ab98b1688d7b03e1"
integrity sha512-iQ5OCvuoaIanbq4GRqj4Azay86mVpa64pP9Oi3EJpaURGZNLqwE7bWq9tkr1Dr7zBPBZN7QBmLD3OOeOSzbHuA==
dependencies:
"@sentry/bundler-plugin-core" "2.18.0"
unplugin "1.0.1"
uuid "^9.0.0"
"@sentry/core" "8.27.0"
"@sentry/types" "8.27.0"
"@sentry/utils" "8.27.0"
"@sentry/webpack-plugin@2.20.1":
version "2.20.1"
@ -5543,27 +5161,6 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/koa@2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.14.0.tgz#8939e8c3b695defc12f2ef9f38064509e564be18"
integrity sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA==
dependencies:
"@types/accepts" "*"
"@types/content-disposition" "*"
"@types/cookies" "*"
"@types/http-assert" "*"
"@types/http-errors" "*"
"@types/keygrip" "*"
"@types/koa-compose" "*"
"@types/node" "*"
"@types/koa__router@12.0.3":
version "12.0.3"
resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-12.0.3.tgz#3fb74ea1991cadd6c6712b6106657aa6e64afca4"
integrity sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw==
dependencies:
"@types/koa" "*"
"@types/lodash@^4.14.175":
version "4.14.195"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz"
@ -5988,6 +5585,11 @@
resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-0.1.11.tgz"
integrity sha512-mj5CPR02y0BRs1tN3oZcBNAX9a8NxsIUl9vElDPcqxnMfP0RbRc9fI9Ud7+QDg/1Izvt5uMumsr+6YsmVHcyuw==
"@vercel/functions@*":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@vercel/functions/-/functions-1.4.1.tgz#d1a81c27f7eb5b5b2ca412a8a4e682e4cfbea32d"
integrity sha512-987mKpoDCI18x2rDjrDbiuGOtxDMK/UhfhDZoN3A2SAt40BMaj8HEKwVqL6fOian5j/3RZcOuJnMOpatQGgpjw==
"@vercel/functions@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@vercel/functions/-/functions-1.0.2.tgz#c26ed4e3b0ed701e28c4ebd71c76b1bfe14db02a"
@ -9344,16 +8946,6 @@ import-fresh@^3.2.1:
parent-module "^1.0.0"
resolve-from "^4.0.0"
import-in-the-middle@1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz#2a266676e3495e72c04bbaa5ec14756ba168391b"
integrity sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==
dependencies:
acorn "^8.8.2"
acorn-import-assertions "^1.9.0"
cjs-module-lexer "^1.2.2"
module-details-from-path "^1.0.3"
import-in-the-middle@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.1.tgz#3e111ff79c639d0bde459bd7ba29dd9fdf357364"
@ -9364,26 +8956,6 @@ import-in-the-middle@1.7.1:
cjs-module-lexer "^1.2.2"
module-details-from-path "^1.0.3"
import-in-the-middle@1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.4.tgz#508da6e91cfa84f210dcdb6c0a91ab0c9e8b3ebc"
integrity sha512-Lk+qzWmiQuRPPulGQeK5qq0v32k2bHnWrRPFgqyvhw7Kkov5L6MOLOIU3pcWeujc9W4q54Cp3Q2WV16eQkc7Bg==
dependencies:
acorn "^8.8.2"
acorn-import-attributes "^1.9.5"
cjs-module-lexer "^1.2.2"
module-details-from-path "^1.0.3"
import-in-the-middle@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz#c94d88d53701de9a248f9710b41f533e67f598a4"
integrity sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ==
dependencies:
acorn "^8.8.2"
acorn-import-attributes "^1.9.5"
cjs-module-lexer "^1.2.2"
module-details-from-path "^1.0.3"
import-in-the-middle@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708"
@ -11270,15 +10842,6 @@ openid-client@^5.4.0:
object-hash "^2.2.0"
oidc-token-hash "^5.0.3"
opentelemetry-instrumentation-fetch-node@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.0.tgz#5beaad33b622f7021c61733af864fb505cd35626"
integrity sha512-aiSt/4ubOTyb1N5C2ZbGrBvaJOXIZhZvpRPYuUVxQJe27wJZqf/o65iPrqgLcgfeOLaQ8cS2Q+762jrYvniTrA==
dependencies:
"@opentelemetry/api" "^1.6.0"
"@opentelemetry/instrumentation" "^0.43.0"
"@opentelemetry/semantic-conventions" "^1.17.0"
opentelemetry-instrumentation-fetch-node@1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.3.tgz#beb24048bdccb1943ba2a5bbadca68020e448ea7"