Merge branch 'main' into feat/add-recipient-groups

This commit is contained in:
David Nguyen
2026-08-30 22:56:44 +10:00
committed by GitHub
54 changed files with 1115 additions and 116 deletions
@@ -0,0 +1,61 @@
name: 'One-Click Deploy Provider Request'
description: Request a new one-click deployment provider (Railway, Render, etc.) to be added to our README
title: 'One-Click Deploy Provider Request: [Provider Name]'
labels: ['deploy-provider-request']
body:
- type: markdown
attributes:
value: |
Thanks for your interest in adding a one-click deploy option for Documenso!
Each provider we list requires us to create, test, and maintain a deployment template, which is ongoing work on top of everything else. To keep this manageable, we ask that providers (or users) **open an issue instead of a PR** so the community can signal interest.
**How this works:**
- 👍 this issue if you'd like to see Documenso deployable on this provider.
- If community interest is high enough, we'll consider adding it to the README.
- Opening an issue is not a guarantee of inclusion. PRs adding badges without a prior issue and demonstrated interest will be closed.
- type: input
attributes:
label: Provider Name
placeholder: e.g. Railway
validations:
required: true
- type: input
attributes:
label: Provider Website
placeholder: e.g. https://railway.com
validations:
required: true
- type: input
attributes:
label: Deploy/Template URL
description: A link to an existing deployment template or deploy button URL, if one exists.
- type: dropdown
attributes:
label: Who creates and maintains the deployment template?
options:
- The provider
- Me / the community
- Nobody yet
validations:
required: true
- type: textarea
attributes:
label: Testing & Maintenance
description: Has the template been tested against the current Documenso release? How are updates handled when Documenso ships breaking changes (env vars, migrations, Docker changes)?
validations:
required: true
- type: textarea
attributes:
label: Why this provider?
description: Tell us why Documenso users would benefit — existing user base, region coverage, free tier, etc.
validations:
required: true
- type: checkboxes
attributes:
label: Please check the boxes that apply to this request.
options:
- label: I have searched existing issues to make sure this provider has not already been requested.
- label: I understand that inclusion depends on community interest and is not guaranteed.
- label: I understand that PRs adding deploy badges without a prior issue will be closed.
+30 -14
View File
@@ -186,21 +186,37 @@ For full instructions, requirements, and configuration details, see the [Self Ho
### One-Click Deploys
#### Railway
> [!NOTE]
> Want to see another provider listed here? Please [open a provider request](https://github.com/documenso/documenso/issues/new?template=deploy-provider-request.yml) instead of a PR so the community can signal interest. PRs adding deploy badges without a prior issue will be closed.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
#### Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)
#### Koyeb
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
#### Elestio
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/documenso)
<table>
<tr>
<td align="center" width="200">
<a href="https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic">
<img src="https://railway.com/button.svg" alt="Deploy on Railway" height="40" />
</a>
</td>
<td align="center" width="200">
<a href="https://render.com/deploy?repo=https://github.com/documenso/documenso">
<img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" height="40" />
</a>
</td>
<td align="center" width="200">
<a href="https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile">
<img src="https://www.koyeb.com/static/images/deploy/button.svg" alt="Deploy to Koyeb" height="40" />
</a>
</td>
</tr>
<tr>
<td align="center" width="200">
<a href="https://elest.io/open-source/documenso">
<img src="https://elest.io/images/logos/deploy-to-elestio-btn.png" alt="Deploy on Elestio" height="40" />
</a>
</td>
<td align="center" width="200"></td>
<td align="center" width="200"></td>
</tr>
</table>
## Security
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -71,6 +72,7 @@ export const EnvelopeDistributeDialog = ({
const { toast } = useToast();
const { t, i18n } = useLingui();
const navigate = useNavigate();
const analytics = useAnalytics();
const [isOpen, setIsOpen] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
@@ -200,6 +202,12 @@ export const EnvelopeDistributeDialog = ({
} catch (err) {
const error = AppError.parseError(err);
analytics.captureException(err, {
source: 'editor',
location: 'distribute_document',
envelopeId: envelope.id,
});
const errorMessage = getDistributeErrorMessage(error.code);
toast({
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { AppError } from '@documenso/lib/errors/app-error';
import type { TEnvelope } from '@documenso/lib/types/envelope';
@@ -51,6 +52,7 @@ export const EnvelopeRedistributeDialog = ({ envelope, envelopeType, trigger }:
const { toast } = useToast();
const { t, i18n } = useLingui();
const analytics = useAnalytics();
const [isOpen, setIsOpen] = useState(false);
@@ -95,6 +97,13 @@ export const EnvelopeRedistributeDialog = ({ envelope, envelopeType, trigger }:
setIsOpen(false);
} catch (err) {
const error = AppError.parseError(err);
analytics.captureException(err, {
source: 'editor',
location: 'redistribute_document',
envelopeId: envelope.id,
});
const errorMessage = getDistributeErrorMessage(error.code);
toast({
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
@@ -77,6 +78,7 @@ export const EmbedDirectTemplateClientPage = ({
}: EmbedDirectTemplateClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const [searchParams] = useSearchParams();
@@ -264,6 +266,13 @@ export const EmbedDirectTemplateClientPage = ({
const error = AppError.parseError(err);
const errorMessage = getDirectTemplateErrorMessage(error.code);
analytics.captureException(err, {
source: 'embed',
location: 'direct_template',
recipientId: recipient.id,
envelopeId,
});
toast({
title: _(errorMessage.title),
description: _(errorMessage.description),
@@ -308,6 +317,14 @@ export const EmbedDirectTemplateClientPage = ({
}
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'embed_init',
recipientId: recipient.id,
envelopeId,
});
setHasFinishedInit(true);
}
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
@@ -75,6 +76,7 @@ export const EmbedSignDocumentV1ClientPage = ({
}: EmbedSignDocumentV1ClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const { fullName, email, signature, setFullName, setEmail, setSignature } = useRequiredDocumentSigningContext();
@@ -154,6 +156,14 @@ export const EmbedSignDocumentV1ClientPage = ({
setHasCompletedDocument(true);
} catch (err) {
analytics.captureException(err, {
source: 'embed',
location: 'complete_document',
recipientId: recipient.id,
documentId,
envelopeId,
});
if (window.parent) {
window.parent.postMessage(
{
@@ -236,6 +246,15 @@ export const EmbedSignDocumentV1ClientPage = ({
}
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'embed_init',
recipientId: recipient.id,
documentId,
envelopeId,
});
setHasFinishedInit(true);
}
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
@@ -25,6 +26,7 @@ export const EmbedSignDocumentV2ClientPage = ({
allowWhitelabelling = false,
}: EmbedSignDocumentV2ClientPageProps) => {
const { _ } = useLingui();
const analytics = useAnalytics();
const { envelope, recipient, envelopeData, setFullName, setEmail, fullName, email } =
useRequiredEnvelopeSigningContext();
@@ -170,6 +172,14 @@ export const EmbedSignDocumentV2ClientPage = ({
}
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'embed_init',
recipientId: recipient.id,
envelopeId: envelope.id,
});
setHasFinishedInit(true);
}
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
@@ -57,6 +58,7 @@ export const MultiSignDocumentSigningView = ({
}: MultiSignDocumentSigningViewProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const { fullName, email, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
@@ -101,6 +103,13 @@ export const MultiSignDocumentSigningView = ({
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'sign_field',
recipientId,
documentId: document?.id,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
@@ -120,6 +129,13 @@ export const MultiSignDocumentSigningView = ({
}
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'remove_field',
recipientId,
documentId: document?.id,
});
}
};
@@ -140,6 +156,13 @@ export const MultiSignDocumentSigningView = ({
recipientId,
});
} catch (err) {
analytics.captureException(err, {
source: 'embed',
location: 'complete_document',
recipientId,
documentId: document?.id,
});
onDocumentError?.();
const error = AppError.parseError(err);
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { AppError } from '@documenso/lib/errors/app-error';
import type { TTemplate } from '@documenso/lib/types/template';
@@ -41,6 +42,7 @@ export const DirectTemplatePageView = ({
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const { email, fullName, setEmail } = useRequiredDocumentSigningContext();
const { recipient, setRecipient } = useRequiredDocumentSigningAuthContext();
@@ -124,6 +126,13 @@ export const DirectTemplatePageView = ({
const error = AppError.parseError(err);
const errorMessage = getDirectTemplateErrorMessage(error.code);
analytics.captureException(err, {
source: 'signing',
location: 'direct_template',
recipientId: directTemplateRecipient.id,
envelopeId: template.envelopeId,
});
toast({
title: _(errorMessage.title),
description: _(errorMessage.description),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -39,6 +40,7 @@ export const DocumentSigningCheckboxField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -126,6 +128,13 @@ export const DocumentSigningCheckboxField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -157,6 +166,13 @@ export const DocumentSigningCheckboxField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -216,6 +232,13 @@ export const DocumentSigningCheckboxField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while updating the signature.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { type TRecipientAccessAuth, ZDocumentAccessAuthSchema } from '@documenso/lib/types/document-auth';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
@@ -88,6 +89,7 @@ export const DocumentSigningCompleteDialog = ({
position,
disableNameInput = false,
}: DocumentSigningCompleteDialogProps) => {
const analytics = useAnalytics();
const { t, i18n } = useLingui();
const { toast } = useToast();
@@ -181,6 +183,11 @@ export const DocumentSigningCompleteDialog = ({
return;
}
analytics.captureException(error, {
source: 'signing',
location: 'complete_document',
});
// This dialog owns the completion error toast for every signing surface
// so the user gets a specific, actionable message. Callers should run
// their own side effects (e.g. embeds posting document-error) and
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { convertToLocalSystemFormat, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -39,6 +40,7 @@ export const DocumentSigningDateField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -85,6 +87,13 @@ export const DocumentSigningDateField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -113,6 +122,13 @@ export const DocumentSigningDateField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -35,6 +36,7 @@ export const DocumentSigningDropdownField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -86,6 +88,13 @@ export const DocumentSigningDropdownField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -120,6 +129,13 @@ export const DocumentSigningDropdownField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -33,6 +34,7 @@ export const DocumentSigningEmailField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { email: providedEmail } = useRequiredDocumentSigningContext();
@@ -78,6 +80,13 @@ export const DocumentSigningEmailField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -106,6 +115,13 @@ export const DocumentSigningEmailField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -38,6 +39,7 @@ export const DocumentSigningInitialsField = ({
const { toast } = useToast();
const { _ } = useLingui();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { fullName } = useRequiredDocumentSigningContext();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -84,6 +86,13 @@ export const DocumentSigningInitialsField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -112,6 +121,13 @@ export const DocumentSigningInitialsField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -39,6 +40,7 @@ export const DocumentSigningNameField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { fullName: providedFullName, setFullName: setProvidedFullName } = useRequiredDocumentSigningContext();
@@ -116,6 +118,13 @@ export const DocumentSigningNameField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -144,6 +153,13 @@ export const DocumentSigningNameField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,4 +1,5 @@
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -47,6 +48,7 @@ export const DocumentSigningNumberField = ({ field, onSignField, onUnsignField }
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -142,6 +144,13 @@ export const DocumentSigningNumberField = ({ field, onSignField, onUnsignField }
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -193,6 +202,13 @@ export const DocumentSigningNumberField = ({ field, onSignField, onUnsignField }
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
@@ -90,8 +89,6 @@ export const DocumentSigningPageViewV1 = ({
? authUser.twoFactorEnabled && authUser.email === recipient.email
: false;
const analytics = useAnalytics();
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
const [isExpanded, setIsExpanded] = useState(false);
@@ -125,12 +122,6 @@ export const DocumentSigningPageViewV1 = ({
await completeDocumentWithToken(payload);
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: document.id,
timestamp: new Date().toISOString(),
});
if (documentMeta?.redirectUrl) {
window.location.href = documentMeta.redirectUrl;
} else {
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -31,6 +32,7 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -91,6 +93,13 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -120,6 +129,13 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the selection.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -41,6 +42,7 @@ export function DocumentSigningRejectDialog({
}: DocumentSigningRejectDialogProps) {
const { t } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const [searchParams] = useSearchParams();
const [isOpen, setIsOpen] = useState(false);
@@ -76,6 +78,12 @@ export function DocumentSigningRejectDialog({
window.location.href = `/sign/${token}/rejected`;
}
} catch (err) {
analytics.captureException(err, {
source: 'signing',
location: 'reject_document',
documentId,
});
toast({
title: t`Error`,
description: t`An error occurred while rejecting the document. Please try again.`,
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -47,6 +48,7 @@ export const DocumentSigningSignatureField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient } = useDocumentSigningRecipientContext();
@@ -157,6 +159,13 @@ export const DocumentSigningSignatureField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
@@ -183,6 +192,13 @@ export const DocumentSigningSignatureField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
@@ -1,4 +1,5 @@
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -50,6 +51,7 @@ export const DocumentSigningTextField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -170,6 +172,13 @@ export const DocumentSigningTextField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -200,6 +209,13 @@ export const DocumentSigningTextField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,5 +1,4 @@
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
@@ -38,7 +37,6 @@ export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadBu
const team = useCurrentTeam();
const navigate = useNavigate();
const analytics = useAnalytics();
const organisation = useCurrentOrganisation();
const userTimezone =
@@ -103,12 +101,6 @@ export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadBu
description: _(msg`Your document has been uploaded successfully.`),
duration: 5000,
});
analytics.capture('App: Document Uploaded', {
userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
}
// Handle legacy template creation.
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
@@ -39,6 +40,7 @@ import { EnvelopeRecipientSelectorCommand } from './envelope-recipient-selector'
export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageRenderData }) => {
const { t, i18n } = useLingui();
const analytics = useAnalytics();
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
@@ -276,6 +278,13 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
unsafeRenderFieldOnLayer(field);
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'editor',
location: 'envelope_page_render',
envelopeId: envelope.id,
});
setRenderError(true);
}
};
@@ -1,4 +1,5 @@
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useEnvelopeAutosave } from '@documenso/lib/client-only/hooks/use-envelope-autosave';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
@@ -45,6 +46,7 @@ export const EnvelopeEditorUploadPage = () => {
const { t, i18n } = useLingui();
const { maximumEnvelopeItemCount, remaining } = useLimits();
const { toast } = useToast();
const analytics = useAnalytics();
const {
envelope,
@@ -213,6 +215,12 @@ export const EnvelopeEditorUploadPage = () => {
const { data } = await createPromise.catch((error) => {
console.error(error);
analytics.captureException(error, {
source: isEmbedded ? 'embed' : 'editor',
location: 'create_envelope_items',
envelopeId: envelope.id,
});
// Set error state on files in batch upload.
setLocalFiles((prev) =>
prev.map((uploadingFile) =>
@@ -290,6 +298,12 @@ export const EnvelopeEditorUploadPage = () => {
} catch (error) {
console.error(error);
analytics.captureException(error, {
source: isEmbedded ? 'embed' : 'editor',
location: 'replace_pdf',
envelopeId: envelope.id,
});
toast({
title: t`Replace failed`,
description: t`Something went wrong while replacing the PDF`,
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import {
type PageRenderData,
@@ -18,6 +19,7 @@ type GenericLocalField = TEnvelope['fields'][number] & {
export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRenderData }) => {
const { i18n } = useLingui();
const analytics = useAnalytics();
const {
envelopeStatus,
@@ -114,6 +116,13 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender
unsafeRenderFieldOnLayer(field);
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'editor',
location: 'envelope_page_render',
envelopeId: currentEnvelopeItem?.envelopeId,
});
setRenderError(true);
}
};
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import {
type PageRenderData,
@@ -53,6 +54,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { toast } = useToast();
const analytics = useAnalytics();
const {
envelopeData,
@@ -426,6 +428,14 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
unsafeRenderFieldOnLayer(unparsedField, fieldCanvasStyleCache);
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'page_render',
recipientId: recipient.id,
envelopeId: envelope.id,
});
setRenderError(true);
}
};
@@ -507,6 +517,14 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: payload.type,
recipientId: recipient.id,
envelopeId: envelope.id,
});
toast({
title: t`Error`,
description: t`An error occurred while signing the field.`,
@@ -118,12 +118,6 @@ export const EnvelopeSignerCompleteDialog = () => {
title: t`Document already signed`,
description: t`This document was already signed and no further action was taken.`,
});
} else {
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: envelope.id,
timestamp: new Date().toISOString(),
});
}
if (onDocumentCompleted) {
@@ -148,6 +142,13 @@ export const EnvelopeSignerCompleteDialog = () => {
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
analytics.captureException(err, {
source: 'signing',
location: 'complete_document',
recipientId: recipient.id,
envelopeId: envelope.id,
});
onDocumentError?.();
}
@@ -221,6 +222,13 @@ export const EnvelopeSignerCompleteDialog = () => {
} catch (err) {
console.log('err', err);
analytics.captureException(err, {
source: 'signing',
location: 'complete_document_next_signer',
recipientId: recipient.id,
envelopeId: envelope.id,
});
onDocumentError?.();
// Rethrow so DocumentSigningCompleteDialog can toast a specific
@@ -93,14 +93,6 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
duration: 5000,
});
if (type === EnvelopeType.DOCUMENT) {
analytics.capture('App: Document Uploaded', {
userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
}
const pathPrefix = type === EnvelopeType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
const aiQueryParam = team.preferences.aiFeaturesEnabled ? '?ai=true' : '';
@@ -109,6 +101,11 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
} catch (err) {
const error = AppError.parseError(err);
analytics.captureException(err, {
source: 'editor',
location: 'upload_document',
});
const errorMessage = getUploadErrorMessage(error.code);
toast({
@@ -1,4 +1,5 @@
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
@@ -34,6 +35,7 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
const { t, i18n } = useLingui();
const { toast } = useToast();
const { user } = useSession();
const analytics = useAnalytics();
const team = useCurrentTeam();
@@ -112,6 +114,11 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
console.error(err);
analytics.captureException(err, {
source: 'editor',
location: 'upload_document',
});
const errorMessage = getUploadErrorMessage(error.code);
toast({
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import type { ImageLoadingState, PageRenderData } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_PAGE_CLASSNAME } from '@documenso/lib/constants/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
@@ -68,6 +69,7 @@ export default function PDFViewer({
}: PDFViewerProps) {
const { t } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const $el = useRef<HTMLDivElement>(null);
@@ -150,6 +152,11 @@ export default function PDFViewer({
console.error(err);
setLoadingState('error');
analytics.captureException(err, {
source: 'pdf_viewer',
location: 'pdf_load',
});
toast({
title: t`Error`,
description: t`An error occurred while loading the document.`,
@@ -366,6 +373,8 @@ const PdfViewerPage = ({
* Manages rendering a page from a pdf.
*/
const usePdfPageImage = ({ pageNumber, pdf, scale, scaledWidth, scaledHeight }: PdfViewerPageProps) => {
const analytics = useAnalytics();
const [imageLoadingState, setImageLoadingState] = useState<ImageLoadingState>('loading');
const [imageUrl, setImageUrl] = useState('');
@@ -457,6 +466,12 @@ const usePdfPageImage = ({ pageNumber, pdf, scale, scaledWidth, scaledHeight }:
if (!isCancelled) {
console.error(err);
analytics.captureException(err, {
source: 'pdf_viewer',
location: 'pdf_page_render',
});
setImageLoadingState('error');
}
} finally {
+44
View File
@@ -18,6 +18,23 @@ import './utils/polyfills/promise-with-resolvers';
* the page early, leaving dead event handlers (broken dropdowns, native form
* submits).
*/
/**
* Signing and direct template URLs contain recipient tokens which must never
* be sent to PostHog. Recipient context is attached explicitly via
* `recipientId` where needed instead.
*/
const redactTokensFromUrl = (value: string) => {
return value.replace(/(\/(?:sign|d|direct)\/)([^/?#]+)/g, '$1:token');
};
const URL_EVENT_PROPERTIES = [
'$current_url',
'$pathname',
'$referrer',
'$initial_referrer',
'$prev_pageview_pathname',
] as const;
function initPosthog() {
const postHogConfig = extractPostHogConfig();
@@ -25,7 +42,34 @@ function initPosthog() {
void import('posthog-js').then(({ default: posthog }) => {
posthog.init(postHogConfig.key, {
api_host: postHogConfig.host,
// Only create person profiles for identified (authenticated) users,
// anonymous recipients on signing pages stay anonymous.
person_profiles: 'identified_only',
// Explicit events only, autocapture on signing pages blows up usage
// without providing actionable data.
autocapture: false,
capture_pageview: true,
capture_pageleave: false,
capture_exceptions: true,
before_send: (event) => {
if (!event) {
return null;
}
for (const property of URL_EVENT_PROPERTIES) {
const value = event.properties?.[property];
if (typeof value === 'string') {
event.properties[property] = redactTokensFromUrl(value);
}
}
if (event.$set_once && typeof event.$set_once['$initial_current_url'] === 'string') {
event.$set_once['$initial_current_url'] = redactTokensFromUrl(event.$set_once['$initial_current_url']);
}
return event;
},
});
});
}
+10
View File
@@ -1,4 +1,5 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { SessionProvider } from '@documenso/lib/client-only/providers/session';
import { getBasePath } from '@documenso/lib/constants/app';
import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
@@ -9,6 +10,7 @@ import { getOrganisationSession } from '@documenso/trpc/server/organisation-rout
import { Toaster } from '@documenso/ui/primitives/toaster';
import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';
import { useEffect } from 'react';
import {
data,
isRouteErrorResponse,
@@ -200,11 +202,19 @@ export default function App() {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
if (errorCode !== 404) {
console.error('[RootErrorBoundary]', error);
}
useEffect(() => {
if (errorCode !== 404) {
analytics.captureException(error, { source: 'app', location: 'root_boundary' });
}
}, [error]);
return <GenericErrorLayout errorCode={errorCode} />;
}
@@ -1,4 +1,5 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -7,6 +8,7 @@ import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { ChevronLeftIcon } from 'lucide-react';
import { useEffect } from 'react';
import type { ShouldRevalidateFunctionArgs } from 'react-router';
import { isRouteErrorResponse, Link, Outlet, redirect } from 'react-router';
@@ -66,8 +68,18 @@ export default function DocumentsLayout() {
}
export function ErrorBoundary({ error, params }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, {
source: 'editor',
location: 'editor_layout_boundary',
envelopeId: params.id,
});
}, [error]);
const errorCodeMap = {
404: {
subHeading: msg`404 Document not found`,
@@ -1,4 +1,5 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -8,6 +9,7 @@ import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { ChevronLeftIcon } from 'lucide-react';
import { useEffect } from 'react';
import { isRouteErrorResponse, Link, Outlet, redirect, type ShouldRevalidateFunctionArgs } from 'react-router';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
@@ -84,8 +86,18 @@ export default function TemplatesLayout() {
}
export function ErrorBoundary({ error, params }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, {
source: 'editor',
location: 'editor_layout_boundary',
envelopeId: params.id,
});
}, [error]);
const errorCodeMap = {
404: {
subHeading: msg`404 Template not found`,
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -5,6 +6,7 @@ import { i18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { useEffect } from 'react';
import { isRouteErrorResponse, Link, Outlet } from 'react-router';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
@@ -48,8 +50,14 @@ export default function RecipientLayout({ matches }: Route.ComponentProps) {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, { source: 'signing', location: 'recipient_layout_boundary' });
}, [error]);
return (
<GenericErrorLayout
errorCode={errorCode}
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import {
IS_GOOGLE_SSO_ENABLED,
IS_MICROSOFT_SSO_ENABLED,
@@ -5,6 +6,7 @@ import {
OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth';
import { Trans } from '@lingui/react/macro';
import { useEffect } from 'react';
import { isRouteErrorResponse, Outlet, useRouteError } from 'react-router';
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
@@ -47,10 +49,30 @@ export default function Layout() {
export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData || {};
const analytics = useAnalytics();
const error = useRouteError();
console.log({ routeError: error });
useEffect(() => {
const isExpectedEmbedResponse =
isRouteErrorResponse(error) &&
[
'embed-authentication-required',
'embed-paywall',
'embed-waiting-for-turn',
'embed-recipient-expired',
'embed-document-rejected',
'embed-document-completed',
].includes(error.data?.type);
if (isExpectedEmbedResponse) {
return;
}
analytics.captureException(error, { source: 'embed', location: 'embed_layout_boundary' });
}, [error]);
if (isRouteErrorResponse(error)) {
if (error.status === 401 && error.data.type === 'embed-authentication-required') {
return (
@@ -2,11 +2,13 @@ import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-direct-template-signing';
import { getEnvelopeRequiredAccessData } from '@documenso/lib/server-only/envelope/get-envelope-required-access-data';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { data } from 'react-router';
@@ -93,6 +95,30 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: template.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: template.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: template.envelopeId,
},
});
});
return {
token,
user,
@@ -194,6 +220,30 @@ async function handleV2Loader({ params, request }: Route.LoaderArgs) {
);
}
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: envelope.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: envelope.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: envelope.id,
},
});
});
return {
token,
user,
@@ -2,6 +2,7 @@ import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getEnvelopeForRecipientSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
@@ -13,6 +14,7 @@ import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { isRecipientExpired } from '@documenso/lib/utils/recipients';
@@ -139,6 +141,30 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
})
: [];
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: document.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: document.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: document.envelopeId,
},
});
});
return {
token,
user,
@@ -272,6 +298,30 @@ async function handleV2Loader({ params, request }: Route.LoaderArgs) {
recipientAccessAuth: derivedRecipientAccessAuth,
}).catch(() => null);
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: envelope.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: envelope.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: envelope.id,
},
});
});
return {
token,
user,
@@ -1,8 +1,11 @@
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { ZBaseEmbedAuthoringSchema } from '@documenso/lib/types/embed-authoring-base-schema';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { prisma } from '@documenso/prisma';
import { TrpcProvider } from '@documenso/trpc/react';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { Trans } from '@lingui/react/macro';
@@ -35,6 +38,40 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
});
allowEmbedAuthoringWhiteLabel = organisationClaim.flags.embedAuthoringWhiteLabel ?? false;
// Derive the kind of authoring session from the child route path, e.g.
// /embed/v1/authoring/document/create -> resource 'document', mode 'create'.
// Completed and error pages are not new sessions and are skipped.
const [resource, mode] = url.pathname.split('/').filter(Boolean).slice(3);
const isAuthoringSession =
(resource === 'document' || resource === 'template') && (mode === 'create' || mode === 'edit');
if (isAuthoringSession) {
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: result.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: result.userId,
organisationId: team?.organisationId,
teamId: result.teamId,
properties: {
type: 'authoring',
version: 'v1',
resource,
mode,
},
});
});
}
}
return {
@@ -1,8 +1,11 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { prisma } from '@documenso/prisma';
import { Trans } from '@lingui/react/macro';
import { SigningStatus } from '@prisma/client';
import { useEffect, useLayoutEffect, useState } from 'react';
@@ -42,6 +45,15 @@ export async function loader({ request }: Route.LoaderArgs) {
const firstDocument = envelopes[0]?.document;
if (!firstDocument) {
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
properties: {
type: 'signing',
version: 'v1',
},
});
return superLoaderJson({
envelopes,
user,
@@ -55,6 +67,29 @@ export async function loader({ request }: Route.LoaderArgs) {
const allowWhitelabelling = organisationClaim.flags.embedSigningWhiteLabel;
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: firstDocument.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: firstDocument.teamId,
properties: {
type: 'signing',
version: 'v1',
envelopeId: firstDocument.envelopeId,
},
});
});
return superLoaderJson({
envelopes,
user,
@@ -1,5 +1,6 @@
import { PAID_PLAN_LIMITS } from '@documenso/ee/server-only/limits/constants';
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { OrganisationProvider } from '@documenso/lib/client-only/providers/organisation';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
@@ -12,7 +13,7 @@ import type { OrganisationSession } from '@documenso/trpc/server/organisation-ro
import { Spinner } from '@documenso/ui/primitives/spinner';
import { Trans } from '@lingui/react/macro';
import { OrganisationMemberRole, OrganisationType, TeamMemberRole } from '@prisma/client';
import { useLayoutEffect, useState } from 'react';
import { useEffect, useLayoutEffect, useState } from 'react';
import { isRouteErrorResponse, Outlet, useLoaderData } from 'react-router';
import { match } from 'ts-pattern';
@@ -166,8 +167,17 @@ export default function AuthoringLayout() {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, {
source: 'embed',
location: 'embed_authoring_boundary',
});
}, [error]);
return (
<div>
{match(errorCode)
@@ -1,5 +1,6 @@
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
@@ -10,6 +11,7 @@ import {
ZEmbedCreateEnvelopeAuthoringSchema,
} from '@documenso/lib/types/envelope-editor';
import type { TEnvelopeFieldAndMeta } from '@documenso/lib/types/field-meta';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { extractDerivedDocumentMeta } from '@documenso/lib/utils/document';
import { buildEmbeddedEditorOptions, buildEmbeddedFeatures } from '@documenso/lib/utils/embed-config';
import { prisma } from '@documenso/prisma';
@@ -76,6 +78,30 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
},
});
fireAndForget(async () => {
const team = result.teamId
? await prisma.team.findFirst({
where: {
id: result.teamId,
},
select: {
organisationId: true,
},
})
: null;
captureServerEvent({
event: 'App: Embed Session Started',
userId: result.userId,
organisationId: team?.organisationId,
teamId: result.teamId ?? undefined,
properties: {
type: 'authoring',
version: 'v2',
},
});
});
return superLoaderJson({
token,
tokenUserId: result.userId,
@@ -1,5 +1,6 @@
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getEditorEnvelopeById } from '@documenso/lib/server-only/envelope/get-editor-envelope-by-id';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
@@ -94,6 +95,18 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
brandingLogo = settings.brandingLogo;
}
captureServerEvent({
event: 'App: Embed Session Started',
userId: result.userId,
organisationId: envelope.team.organisationId,
teamId: result.teamId ?? undefined,
properties: {
type: 'authoring',
version: 'v2',
envelopeId: envelope.id,
},
});
return superLoaderJson({
token,
envelope,
@@ -31,18 +31,21 @@ export function useAnalytics() {
};
/**
* Capture an analytic event.
* Capture an exception event.
*
* @param error The error to capture.
* @param properties Properties to attach to the event.
* @param properties Properties to attach to the event, such as `source`, `location`,
* `recipientId` or `envelopeId`. Never attach recipient tokens.
*/
const captureException = (error: Error, properties?: Record<string, unknown>) => {
const captureException = (error: unknown, properties?: Record<string, unknown>) => {
if (!isPostHogEnabled) {
return;
}
const errorToCapture = error instanceof Error ? error : new Error(String(error));
void getPosthog().then(({ default: posthog }) => {
posthog.captureException(error, properties);
posthog.captureException(errorToCapture, properties);
});
};
@@ -23,6 +23,7 @@ import { useSearchParams } from 'react-router';
import type { TDocumentEmailSettings } from '../../types/document-email';
import { formatDocumentsPath, formatTemplatesPath } from '../../utils/teams';
import { useAnalytics } from '../hooks/use-analytics';
import type { TLocalField } from '../hooks/use-editor-fields';
import { useEditorFields } from '../hooks/use-editor-fields';
import { useEditorRecipients } from '../hooks/use-editor-recipients';
@@ -115,6 +116,7 @@ export const EnvelopeEditorProvider = ({
}: EnvelopeEditorProviderProps) => {
const { t } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const [_searchParams, setSearchParams] = useSearchParams();
@@ -289,6 +291,12 @@ export const EnvelopeEditorProvider = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: isEmbedded ? 'embed' : 'editor',
location: 'autosave_recipients',
envelopeId: envelope.id,
});
setAutosaveError(true);
toast({
@@ -352,6 +360,12 @@ export const EnvelopeEditorProvider = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: isEmbedded ? 'embed' : 'editor',
location: 'autosave_fields',
envelopeId: envelope.id,
});
setAutosaveError(true);
toast({
@@ -407,6 +421,12 @@ export const EnvelopeEditorProvider = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: isEmbedded ? 'embed' : 'editor',
location: 'autosave_meta',
envelopeId: envelope.id,
});
setAutosaveError(true);
toast({
@@ -0,0 +1,83 @@
import crypto from 'node:crypto';
import { PostHog } from 'posthog-node';
import { env } from '../../utils/env';
/**
* Server-side product analytics client.
*
* Uses the same PostHog project as the client-side integration
* (NEXT_PUBLIC_POSTHOG_KEY). Not to be confused with the self-hoster
* telemetry client which reports to a separate project.
*
* Events are sent directly to PostHog rather than through the `/ingest`
* reverse proxy used by the browser.
*/
const POSTHOG_HOST = 'https://eu.i.posthog.com';
let client: PostHog | null | undefined;
const getAnalyticsClient = () => {
if (client !== undefined) {
return client;
}
const apiKey = env('NEXT_PUBLIC_POSTHOG_KEY');
// Low volume events, flush immediately so nothing is lost on shutdown.
client = apiKey ? new PostHog(apiKey, { host: POSTHOG_HOST, flushAt: 1 }) : null;
return client;
};
type CaptureServerEventOptions = {
event: string;
/**
* The authenticated user this event belongs to, if any.
*
* Used as the distinct ID so server events line up with client-side
* identified users. Anonymous events (e.g. embed signer sessions) get a
* random distinct ID instead.
*/
userId?: number;
organisationId?: string;
teamId?: number;
properties?: Record<string, unknown>;
};
/**
* Capture a product analytics event from the server.
*
* No-ops when NEXT_PUBLIC_POSTHOG_KEY is not configured. Events are captured
* as anonymous events (no person profile processing) to keep usage costs low,
* cross-referencing is done via the `userId`, `organisationId` and `teamId`
* event properties.
*
* Never pass recipient tokens or document contents as properties.
*/
export const captureServerEvent = ({
event,
userId,
organisationId,
teamId,
properties = {},
}: CaptureServerEventOptions) => {
const posthog = getAnalyticsClient();
if (!posthog) {
return;
}
posthog.capture({
distinctId: userId ? String(userId) : `anon_${crypto.randomUUID()}`,
event,
properties: {
...properties,
userId,
organisationId,
teamId,
$process_person_profile: false,
},
});
};
+99 -62
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-08-13 14:22\n"
"PO-Revision-Date: 2026-08-27 12:58\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -316,7 +316,7 @@ msgstr "Sprawdź i {recipientActionVerb} dokument „{1}” utworzony przez zesp
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "{0} has invited you to sign this document."
msgstr ""
msgstr "Organizacja {0} zaprosiła Cię do podpisania dokumentu."
#. placeholder {0}: team.name
#: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
@@ -442,7 +442,7 @@ msgstr "Sprawdź i {action} dokument „{documentName}” utworzony przez użytk
#: apps/remix/app/components/dialogs/envelopes-bulk-download-dialog.tsx
msgid "{MAX_BULK_DOWNLOAD_ENVELOPES, plural, one {You can download up to # document at a time. Deselect some documents to continue.} other {You can download up to # documents at a time. Deselect some documents to continue.}}"
msgstr ""
msgstr "{MAX_BULK_DOWNLOAD_ENVELOPES, plural, one {Możesz pobrać jednocześnie # dokument. Odznacz niektóre dokumenty.} few {Możesz pobrać jednocześnie # dokumenty. Odznacz niektóre dokumenty.} many {Możesz pobrać jednocześnie # dokumentów. Odznacz niektóre dokumenty.} other {Możesz pobrać jednocześnie # dokumentów. Odznacz niektóre dokumenty.}}"
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "{MAXIMUM_PASSKEYS, plural, one {You cannot have more than # passkey.} other {You cannot have more than # passkeys.}}"
@@ -525,11 +525,11 @@ msgstr "Sprawdź i {action} dokument „{documentName}” utworzony przez zespó
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "{totalVisibleCount, plural, one {# item} other {# items}}"
msgstr ""
msgstr "{totalVisibleCount, plural, one {# element} few {# elementy} many {# elementów} other {# elementów}}"
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "{totalVisibleCount, plural, one {# result} other {# results}}"
msgstr ""
msgstr "{totalVisibleCount, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyników}}"
#: packages/lib/utils/document-audit-logs.ts
msgid "{user} added a field"
@@ -1237,7 +1237,7 @@ msgstr "Konto zostało rozłączone"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Accounts are automatically added to your organisation on sign-in"
msgstr ""
msgstr "Automatyczne dodawanie kont do organizacji podczas logowania"
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Acknowledgment"
@@ -2455,7 +2455,7 @@ msgstr "Obramowanie"
#: apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
msgid "Border radius"
msgstr "Zaokrąglenia obramowania"
msgstr "Zaokrąglenie obramowania"
#: apps/remix/app/components/forms/branding-preferences-form.tsx
msgid "Border Radius"
@@ -2471,7 +2471,7 @@ msgstr "Dół"
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Brand accent"
msgstr ""
msgstr "Kolor marki"
#: apps/remix/app/components/forms/branding-preferences-form.tsx
msgid "Brand Colours"
@@ -2479,7 +2479,7 @@ msgstr "Kolory marki"
#: apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
msgid "Brand colours, including background, foreground, primary, and border colours"
msgstr "Kolorów marki, w tym kolorów tła, przycisków i obramowań"
msgstr "Kolory marki, w tym kolory tła, przycisków i obramowań"
#: apps/remix/app/components/forms/branding-preferences-form.tsx
msgid "Brand Details"
@@ -2491,7 +2491,7 @@ msgstr "Strona internetowa marki"
#: apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
msgid "Brand website and brand details"
msgstr "Strony internetowej i szczegółów marki"
msgstr "Strona internetowa i szczegóły marki"
#: apps/remix/app/components/general/admin-global-settings-section.tsx
#: packages/lib/utils/settings-nav.ts
@@ -2581,7 +2581,7 @@ msgstr "Akceptując prośbę, umożliwisz zespołowi <0>{teamName}</0>:"
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "By deleting this document, the following will occur:"
msgstr "Usunięcie dokumentu spowoduje następujące skutki:"
msgstr "Po usunięciu dokumentu:"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
msgid "By enabling 2FA, you will be required to enter a code from your authenticator app every time you sign in using email password."
@@ -3008,11 +3008,11 @@ msgstr "Komunikacja"
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Company details"
msgstr ""
msgstr "Szczegóły firmy"
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Company details and website in email footers"
msgstr ""
msgstr "Szczegóły firmy i strona internetowa w stopkach"
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "Compare all plans and features in detail"
@@ -3203,7 +3203,7 @@ msgstr "Informacje kontaktowe"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Contact Sales"
msgstr ""
msgstr "Skontaktuj się z działem sprzedaży"
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "Contact sales here"
@@ -3215,7 +3215,7 @@ msgstr "Skontaktuj się z nami"
#: apps/remix/app/components/general/settings-upsell/settings-upsell-card.tsx
msgid "Contact your organisation owner to upgrade plans."
msgstr ""
msgstr "Skontaktuj się z właścicielem organizacji, aby zaktualizować plan."
#: apps/remix/app/components/general/admin-site-banner-section.tsx
msgid "Content"
@@ -3271,7 +3271,7 @@ msgstr "Przejdź do logowania"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Continue with SSO"
msgstr ""
msgstr "Logowanie SSO"
#: apps/remix/app/components/forms/reminder-preferences-form.tsx
msgid "Controls how long recipients have to complete signing before the document expires. After expiration, recipients can no longer sign the document."
@@ -3718,11 +3718,11 @@ msgstr "Niestandardowy plik {0} MB"
#: apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
msgid "Custom branding enabled setting"
msgstr "Niestandardowych ustawień marki"
msgstr "Niestandardowe ustawienia marki"
#: apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
msgid "Custom CSS"
msgstr "Niestandardowego CSS"
msgstr "Niestandardowy CSS"
#: apps/remix/app/components/forms/branding-preferences-form.tsx
msgid "Custom CSS is sanitised on save. Layout-breaking properties, remote URLs, and pseudo-elements are stripped automatically. Any rules dropped during sanitisation will be shown after you save."
@@ -4376,7 +4376,7 @@ msgstr "Wszystkie dokumenty"
#: apps/remix/app/utils/toast-error-messages.ts
msgid "Document already completed"
msgstr ""
msgstr "Dokument został już zakończony"
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
msgid "Document already signed"
@@ -4565,7 +4565,7 @@ msgstr "Przeniesiono dokument do zespołu"
#: apps/remix/app/utils/toast-error-messages.ts
msgid "Document no longer available"
msgstr ""
msgstr "Dokument nie jest już dostępny"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document no longer available to sign"
@@ -4573,7 +4573,7 @@ msgstr "Dokument nie jest już dostępny do podpisania"
#: apps/remix/app/utils/toast-error-messages.ts
msgid "Document not ready"
msgstr ""
msgstr "Dokument nie jest gotowy"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
@@ -5033,7 +5033,7 @@ msgstr "np. Resend (plan darmowy)"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Easy DNS setup with auto-generated DKIM and SPF records"
msgstr ""
msgstr "Łatwa konfiguracja DNS z automatycznie generowanymi rekordami DKIM i SPF"
#: apps/remix/app/components/general/document/document-page-view-button.tsx
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
@@ -5709,7 +5709,7 @@ msgstr "Wystąpił błąd podczas przesyłania pliku"
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "esc"
msgstr "escape"
msgstr "esc"
#: apps/remix/app/components/general/webhook-logs-sheet.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id._index.tsx
@@ -6202,7 +6202,7 @@ msgstr "Niemiecki"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Give your members a dedicated single sign-on portal. The SSO portal is available on the Enterprise plan."
msgstr ""
msgstr "Zapewnij swoim użytkownikom logowanie SSO. Funkcja dostępna w planie Enterprise."
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
@@ -6438,7 +6438,7 @@ msgstr "Ukryj"
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
msgid "Hide field"
msgstr ""
msgstr "Ukryj pole"
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "Hide license key"
@@ -6852,7 +6852,7 @@ msgstr "To nie jest Twoja kolej na podpisanie dokumentu. Gdy nadejdzie Twoja kol
#: apps/remix/app/utils/toast-error-messages.ts
msgid "It's not your turn to sign yet"
msgstr ""
msgstr "To nie jest Twoja kolej na podpisanie dokumentu"
#: packages/lib/constants/i18n.ts
msgid "Italian"
@@ -6974,7 +6974,7 @@ msgstr "Użyto"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
msgid "Last Used"
msgstr ""
msgstr "Użyto"
#: apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx
#: apps/remix/app/routes/_authenticated+/admin+/email-domains.$id.tsx
@@ -7603,7 +7603,7 @@ msgstr "Ustawienia nazwy"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Named senders with defaults per team, template or document"
msgstr ""
msgstr "Zdefiniowani nadawcy z domyślnymi ustawieniami dla zespołu, szablonu lub dokumentu"
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "Navigate"
@@ -7990,14 +7990,14 @@ msgstr "Na tej stronie możesz utworzyć nowe webhooki i zarządzać obecnymi."
#: apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
#: apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx
msgid "Once confirmed, the following will be reset:"
msgstr "Potwierdzenie akcji spowoduje zresetowanie:"
msgstr "Po potwierdzeniu zresetowane zostaną:"
#: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx
#: apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx
msgid "Once confirmed, the following will occur:"
msgstr "Potwierdzenie akcji spowoduje następujące skutki:"
msgstr "Po potwierdzeniu:"
#: packages/lib/constants/template.ts
msgid "Once enabled, you can select any active recipient to be a direct link signing recipient, or create a new one. This recipient type cannot be edited or deleted."
@@ -8021,7 +8021,7 @@ msgstr "Jeden z dokumentów ma niezgodną rolę w procesie podpisywania."
#: apps/remix/app/utils/toast-error-messages.ts
msgid "One or more of your required fields have not been saved. Please refresh the page, complete any empty required fields, and try again."
msgstr ""
msgstr "Co najmniej jedno z wymaganych pól nie zostało zapisane. Odśwież stronę, uzupełnij wymagane pola i spróbuj ponownie."
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "Only admins can access and view the document"
@@ -8756,7 +8756,7 @@ msgstr "Wybierz, w jaki sposób chcesz otrzymać swój kod weryfikacyjny."
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Please sign: Example.pdf"
msgstr ""
msgstr "Podpisz dokument: umowa.pdf"
#: apps/remix/app/components/dialogs/organisation-email-domain-create-dialog.tsx
msgid "Please try a different domain."
@@ -8789,6 +8789,10 @@ msgstr "Prześlij dokument, aby kontynuować"
msgid "Please upload a logo"
msgstr "Prześlij logo"
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Please upload a valid image file."
msgstr "Prześlij prawidłową grafikę."
#: packages/lib/constants/i18n.ts
msgid "Polish"
msgstr "Polski"
@@ -8946,7 +8950,7 @@ msgstr "Szablony publiczne są powiązane z Twoim profilem publicznym. Wszelkie
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Put your own brand on every document you send. Branding is available on the Teams plan and above."
msgstr ""
msgstr "Umieść swoje logo na każdym wysyłanym dokumencie. Branding jest dostępny w planie Teams i wyższym."
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Quick Actions"
@@ -9278,7 +9282,7 @@ msgstr "Przekierowywanie do {0}..."
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Redirecting to your identity provider"
msgstr ""
msgstr "Przekierowywanie do dostawcy tożsamości"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
@@ -9650,7 +9654,7 @@ msgstr "Zapewniamy, że Twój dokument jest poufny i nigdy nie zostanie udostęp
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Restrict sign-ins by email domain and choose the default role"
msgstr ""
msgstr "Ogranicz logowanie do wybranych domen i przypisz domyślną rolę"
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
msgid "Restricted Access"
@@ -9814,7 +9818,7 @@ msgstr "Szukaj identyfikatora lub nazwy"
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
msgid "Search by document title, recipient:123, team:123 or user:123"
msgstr ""
msgstr "Szukaj tytułu dokumentu, odbiorcy, zespołu lub użytkownika"
#: apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx
msgid "Search by domain or organisation name"
@@ -10112,7 +10116,7 @@ msgstr "Wybierz widoczność"
#: apps/remix/app/components/tables/envelopes-table-bulk-action-bar.tsx
msgid "selected"
msgstr ""
msgstr "zaznaczono"
#: apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx
msgid "Selected documents will be permanently deleted"
@@ -10170,7 +10174,7 @@ msgstr "Wyślij dokument"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Send documents from your own domain. Email domains are available on the Enterprise plan."
msgstr ""
msgstr "Wysyłaj dokumenty ze swojej domeny. Domeny są dostępne w planie Enterprise."
#: packages/email/templates/confirm-team-email.tsx
msgid "Send documents on behalf of the team using the email address"
@@ -10182,7 +10186,7 @@ msgstr "Wyślij dokumenty do odbiorców natychmiast"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Send emails to recipients from your domain"
msgstr ""
msgstr "Wysyłaj wiadomości ze swojej domeny"
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
msgid "Send Envelope"
@@ -10232,11 +10236,11 @@ msgstr "Nadawca został zgłoszony"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Sending from app.documenso.com"
msgstr ""
msgstr "Wysyłanie z app.documenso.com"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Sending from your domain"
msgstr ""
msgstr "Wysyłanie z Twojej domeny"
#: apps/remix/app/components/forms/forgot-password.tsx
msgid "Sending Reset Email..."
@@ -10260,7 +10264,7 @@ msgstr "Wysłano w tym okresie"
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Separate branding per team"
msgstr ""
msgstr "Oddzielny branding dla zespołów"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Session revoked"
@@ -10501,6 +10505,7 @@ msgid "Signature ID"
msgstr "Identyfikator podpisu"
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Signature is too small"
msgstr "Podpis jest zbyt mały"
@@ -10541,7 +10546,7 @@ msgstr "Podpisany"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Signed in"
msgstr ""
msgstr "Zalogowano"
#: apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx
msgid "Signer"
@@ -10609,7 +10614,7 @@ msgstr "Logowanie..."
#: apps/remix/app/utils/toast-error-messages.ts
msgid "Signing link expired"
msgstr ""
msgstr "Link do podpisywania wygasł"
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
@@ -10665,7 +10670,7 @@ msgstr "Od {0}"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Single sign-on"
msgstr ""
msgstr "Logowanie SSO"
#: apps/remix/app/components/general/admin-site-banner-section.tsx
msgid "Site Banner"
@@ -10688,7 +10693,7 @@ msgstr "Niektóre pola są umieszczone jedno nad drugim. Może to utrudnić podp
#: apps/remix/app/utils/toast-error-messages.ts
msgid "Some fields were not saved"
msgstr ""
msgstr "Niektóre pola nie zostały zapisane"
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "Some searches failed — results may be incomplete."
@@ -11605,7 +11610,7 @@ msgstr "Dokument został odrzucony w zewnętrznej usłudze w imieniu odbiorcy"
#: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
msgid "The document will be hidden from your account"
msgstr "Dokument zostanie ukryty w Twoim koncie"
msgstr "Dokument zostanie ukryty na Twoim koncie"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "The document will be immediately sent to recipients if this is checked."
@@ -11697,6 +11702,14 @@ msgstr "Brak pola podpisu dla następujących podpisujących:"
msgid "The following team has been deleted. You will no longer be able to access this team and its documents"
msgstr "Następujący zespół został usunięty. Nie będziesz mógł już uzyskać dostępu do tego zespołu i jego dokumentów"
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "The image could not be loaded. Please try again."
msgstr "Nie można załadować grafiki. Spróbuj ponownie."
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "The image must be smaller than 5MB."
msgstr "Maksymalny rozmiar grafiki to 5 MB."
#: apps/remix/app/components/general/admin-license-card.tsx
msgid "The license key has been copied to your clipboard"
msgstr "Klucz licencyjny został skopiowany do schowka"
@@ -12027,7 +12040,7 @@ msgstr "Szablon bezpośredniego linku nie może zostać użyty, ponieważ co naj
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document can no longer be signed. It may have been removed by the sender, or your signing access may have been revoked. Please contact the sender for a new signing link."
msgstr ""
msgstr "Nie można podpisać dokumentu. Być może został on usunięty przez nadawcę lub Twoje uprawnienia do podpisywania zostały unieważnione. Skontaktuj się z nadawcą, aby uzyskać nowy link do podpisywania."
#: packages/email/template-components/template-document-super-delete.tsx
msgid "This document can not be recovered, if you would like to dispute the reason for future documents please contact support."
@@ -12059,7 +12072,7 @@ msgstr "Nie można zapisać dokumentu jako szablonu. Spróbuj ponownie."
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document has already been completed and no further signatures can be added."
msgstr ""
msgstr "Dokument został już zakończony i nie wymaga kolejnych podpisów."
#: apps/remix/app/components/general/envelope-editor/envelope-recipient-selector.tsx
#: packages/ui/primitives/recipient-selector.tsx
@@ -12081,7 +12094,7 @@ msgstr "Dokument został anulowany przez właściciela."
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document has been cancelled by the sender and can no longer be signed. Please contact the sender if you believe this is a mistake."
msgstr ""
msgstr "Dokument został anulowany przez nadawcę i nie można go już podpisać. Jeśli uważasz to za błąd, skontaktuj się z nadawcą."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
@@ -12089,7 +12102,7 @@ msgstr "Dokument został odrzucony przez odbiorcę"
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document has been rejected by a recipient and can no longer be signed."
msgstr ""
msgstr "Dokument został odrzucony przez odbiorcę i nie można go już podpisać."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
@@ -12097,7 +12110,7 @@ msgstr "Dokument został podpisany przez wszystkich odbiorców"
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document has not been sent for signing yet. Please wait for the sender to send it before signing."
msgstr ""
msgstr "Dokument nie został jeszcze wysłany do podpisania. Poczekaj na wysłanie go przez nadawcę."
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document has too many recipients. Please remove some recipients or contact support if you need more."
@@ -12113,7 +12126,7 @@ msgstr "Dokument jest szkicem i nie został wysłany"
#: apps/remix/app/utils/toast-error-messages.ts
msgid "This document is signed in a set order and other recipients must sign before you. You will receive an email when it is your turn."
msgstr ""
msgstr "Dokument jest podpisywany zgodnie z ustaloną kolejnością. Gdy nadejdzie Twoja kolej, otrzymasz wiadomość."
#: apps/remix/app/components/general/legacy-field-warning-popover.tsx
msgid "This document is using legacy field insertion, we recommend using the new field insertion method for more accurate results."
@@ -12192,6 +12205,10 @@ msgstr "Typ pliku nie jest obsługiwany. Prześlij dokument PDF lub Word."
msgid "This folder contains multiple items. Deleting it will remove all subfolders and move all nested documents and templates to the root folder."
msgstr "Folder zawiera wiele elementów. Usunięcie go spowoduje przeniesienie wszystkich dokumentów i szablonów znajdujących się w tym folderze do folderu głównego."
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "This image is invalid, please upload a valid image file."
msgstr "Grafika jest nieprawidłowa. Prześlij prawidłowy plik."
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "This is how the document will reach the recipients once the document is ready for signing."
@@ -12774,6 +12791,10 @@ msgstr "Nie można się zalogować"
msgid "Unable to start the signing flow"
msgstr "Nie można rozpocząć procesu podpisywania"
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Unable to upload image"
msgstr "Nie można przesłać grafiki"
#: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
#: apps/remix/app/components/general/document-signing/document-signing-auth-password.tsx
@@ -12788,6 +12809,7 @@ msgstr "Niezakończono"
#: apps/remix/app/components/forms/form-sticky-save-bar.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Undo"
msgstr "Cofnij"
@@ -12832,15 +12854,15 @@ msgstr "Rozłącz"
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Unlock Branding Preferences"
msgstr ""
msgstr "Odblokuj ustawienia brandingu"
#: apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx
msgid "Unlock Email Domains"
msgstr ""
msgstr "Odblokuj domeny"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Unlock the Organisation SSO Portal"
msgstr ""
msgstr "Odblokuj logowanie SSO organizacji"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Unpin"
@@ -13017,7 +13039,7 @@ msgstr "Ulepsz organizację <0>{0}</0> do planu {planName}"
#: apps/remix/app/components/general/settings-upsell/settings-upsell-card.tsx
msgid "Upgrade Plan"
msgstr ""
msgstr "Ulepsz plan"
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
msgid "Upgrade your plan to upload more documents"
@@ -13078,6 +13100,10 @@ msgstr "Prześlij dokumenty i dodaj odbiorców"
msgid "Upload failed"
msgstr "Przesyłanie nie powiodło się"
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload New Image"
msgstr "Prześlij nową grafikę"
#: apps/remix/app/components/general/admin-global-settings-section.tsx
msgid "Upload signature"
msgstr "Podpis przesyłany"
@@ -13506,7 +13532,7 @@ msgstr "Oczekiwanie na innych"
#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts
msgid "Waiting for others to complete signing"
msgstr ""
msgstr "Oczekiwanie na zakończenie podpisywania przez innych"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Waiting for others to sign"
@@ -14004,7 +14030,7 @@ msgstr "Witaj z powrotem! Oto podsumowanie Twojego konta."
#. placeholder {0}: organisation.name
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Welcome to {0}"
msgstr ""
msgstr "Witaj w organizacji {0}"
#: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx
msgid "Welcome to {organisationName}"
@@ -14085,7 +14111,7 @@ msgstr "W ramach limitu"
#: apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx
msgid "Works with any OIDC provider — Okta, Entra ID, Google and more"
msgstr ""
msgstr "Działa z dowolnym dostawcą OIDC: Okta, Entra ID, Google i więcej"
#: apps/remix/app/components/forms/public-profile-form.tsx
msgid "Write a description to display on your public profile"
@@ -15102,7 +15128,7 @@ msgstr "Koperta została ponownie wysłana."
#: apps/remix/app/components/general/settings-upsell/branding-upsell.tsx
msgid "Your logo on signing pages and emails"
msgstr ""
msgstr "Twoje logo na stronach podpisywania i w wiadomościach"
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "Your Name"
@@ -15239,7 +15265,7 @@ msgstr "Certyfikat podpisu jest nieważny, wygasł lub brakuje w nim wymaganego
#: apps/remix/app/utils/toast-error-messages.ts
msgid "Your signing link has expired. Please contact the sender to request a new one."
msgstr ""
msgstr "Twój link do podpisywania wygasł. Skontaktuj się z nadawcą, aby otrzymać nowy link."
#: packages/lib/utils/document-audit-logs.ts
msgid "Your signing provider authentication failed"
@@ -15322,3 +15348,14 @@ msgstr "Twój kod weryfikacyjny:"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx
msgid "your-domain.com another-domain.com"
msgstr "twoja-domena.pl inna-domena.pl"
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Zoom in"
msgstr "Powiększ"
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Zoom out"
msgstr "Pomniejsz"
+14
View File
@@ -0,0 +1,14 @@
/**
* Run a function without awaiting it or letting it affect the caller.
*
* Both synchronous throws and promise rejections are swallowed, making this
* safe to call from request handlers and loaders for side work such as
* analytics that must never block or fail the response.
*/
export const fireAndForget = (fn: () => unknown) => {
try {
void Promise.resolve(fn()).catch(() => null);
} catch {
// Swallow synchronous errors.
}
};
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { renderCustomEmailTemplate } from './render-custom-email-template';
describe('renderCustomEmailTemplate', () => {
it('replaces a single variable', () => {
expect(renderCustomEmailTemplate('Hi {name}', { name: 'Sam' })).toBe('Hi Sam');
});
it('replaces multiple variables separated by whitespace', () => {
expect(
renderCustomEmailTemplate('Hi {name}, sign at {url}', { name: 'Sam', url: 'https://x' }),
).toBe('Hi Sam, sign at https://x');
});
it('replaces adjacent variables and variables separated by a non-whitespace character', () => {
expect(
renderCustomEmailTemplate('{day}/{month}/{year}', { day: '01', month: '02', year: '2026' }),
).toBe('01/02/2026');
expect(
renderCustomEmailTemplate('{firstName}-{lastName}', {
firstName: 'Ada',
lastName: 'Lovelace',
}),
).toBe('Ada-Lovelace');
});
it('leaves the key in place when the variable is not provided', () => {
expect(renderCustomEmailTemplate('Unknown {missing} here', {})).toBe('Unknown missing here');
});
});
@@ -1,5 +1,9 @@
export const renderCustomEmailTemplate = <T extends Record<string, string>>(template: string, variables: T): string => {
return template.replace(/\{(\S+)\}/g, (_, key) => {
// Match a run of non-brace, non-whitespace characters between braces. Using
// `\S+` here was greedy across braces, so placeholders separated by a
// non-whitespace character (e.g. "{day}/{month}/{year}") were captured as a
// single bogus key and left unrendered.
return template.replace(/\{([^\s{}]+)\}/g, (_, key) => {
if (key in variables) {
return variables[key];
}
@@ -1,4 +1,7 @@
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import { ZCreateApiTokenRequestSchema, ZCreateApiTokenResponseSchema } from './create-api-token.types';
@@ -15,10 +18,26 @@ export const createApiTokenRoute = authenticatedProcedure
},
});
return await createApiToken({
const createdToken = await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
expiresIn: expirationDate,
});
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: { id: teamId },
select: { organisationId: true },
});
captureServerEvent({
event: 'App: API Token Created',
userId: ctx.user.id,
teamId,
organisationId: team?.organisationId,
});
});
return createdToken;
});
+23 -1
View File
@@ -1,6 +1,7 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { convertToPdf } from '@documenso/lib/server-only/document-conversion';
@@ -21,12 +22,14 @@ import { findTemplates } from '@documenso/lib/server-only/template/find-template
import { getOrganisationTemplateById } from '@documenso/lib/server-only/template/get-organisation-template-by-id';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import type { Envelope } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
@@ -744,7 +747,7 @@ export const templateRouter = router({
});
}
return await createTemplateDirectLink({
const directLink = await createTemplateDirectLink({
userId,
teamId,
id: {
@@ -753,6 +756,25 @@ export const templateRouter = router({
},
directRecipientId,
});
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: { id: template.teamId },
select: { organisationId: true },
});
captureServerEvent({
event: 'App: Template Direct Link Enabled',
userId,
teamId: template.teamId,
organisationId: team?.organisationId,
properties: {
envelopeId: template.envelopeId,
},
});
});
return directLink;
}),
/**
+23 -1
View File
@@ -1,9 +1,12 @@
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook';
import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id';
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
import { triggerTestWebhook } from '@documenso/lib/server-only/webhooks/trigger-test-webhook';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure, router } from '../trpc';
import { findWebhookCallsRoute } from './find-webhook-calls';
@@ -51,7 +54,7 @@ export const webhookRouter = router({
createWebhook: authenticatedProcedure.input(ZCreateWebhookRequestSchema).mutation(async ({ input, ctx }) => {
const { enabled, eventTriggers, secret, webhookUrl } = input;
return await createWebhook({
const webhook = await createWebhook({
enabled,
secret,
webhookUrl,
@@ -59,6 +62,25 @@ export const webhookRouter = router({
teamId: ctx.teamId,
userId: ctx.user.id,
});
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: { id: ctx.teamId },
select: { organisationId: true },
});
captureServerEvent({
event: 'App: Webhook Created',
userId: ctx.user.id,
teamId: ctx.teamId,
organisationId: team?.organisationId,
properties: {
triggerCount: eventTriggers.length,
},
});
});
return webhook;
}),
deleteWebhook: authenticatedProcedure.input(ZDeleteWebhookRequestSchema).mutation(async ({ input, ctx }) => {