Merge branch 'next' of https://github.com/5h0ov/plunk into multipart/related-support-embed-image
This commit is contained in:
@@ -29,10 +29,6 @@ async function processProjectSegments(projectId: string, projectName?: string):
|
|||||||
const trackedSegments = segments.filter(s => s.trackMembership);
|
const trackedSegments = segments.filter(s => s.trackMembership);
|
||||||
const nonTrackedSegments = segments.filter(s => !s.trackMembership);
|
const nonTrackedSegments = segments.filter(s => !s.trackMembership);
|
||||||
|
|
||||||
signale.info(
|
|
||||||
`[SEGMENT-COUNT-WORKER] Project ${logPrefix}: ${trackedSegments.length} tracked, ${nonTrackedSegments.length} non-tracked segments`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process tracked segments with full membership computation (creates events)
|
// Process tracked segments with full membership computation (creates events)
|
||||||
let updatedSegmentCount = 0;
|
let updatedSegmentCount = 0;
|
||||||
let totalAdded = 0;
|
let totalAdded = 0;
|
||||||
@@ -41,13 +37,7 @@ async function processProjectSegments(projectId: string, projectName?: string):
|
|||||||
if (trackedSegments.length > 0) {
|
if (trackedSegments.length > 0) {
|
||||||
for (const segment of trackedSegments) {
|
for (const segment of trackedSegments) {
|
||||||
try {
|
try {
|
||||||
signale.info(
|
|
||||||
`[SEGMENT-COUNT-WORKER] Computing membership for tracked segment "${segment.name}" (${segment.id})`,
|
|
||||||
);
|
|
||||||
const result = await SegmentService.computeMembership(projectId, segment.id);
|
const result = await SegmentService.computeMembership(projectId, segment.id);
|
||||||
signale.success(
|
|
||||||
`[SEGMENT-COUNT-WORKER] Segment "${segment.name}": +${result.added} entries, -${result.removed} exits, ${result.total} total members`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Track segments with actual changes for bundled notification
|
// Track segments with actual changes for bundled notification
|
||||||
if (result.added > 0 || result.removed > 0) {
|
if (result.added > 0 || result.removed > 0) {
|
||||||
@@ -77,7 +67,6 @@ async function processProjectSegments(projectId: string, projectName?: string):
|
|||||||
if (nonTrackedSegments.length > 0) {
|
if (nonTrackedSegments.length > 0) {
|
||||||
try {
|
try {
|
||||||
await SegmentService.refreshAllMemberCounts(projectId);
|
await SegmentService.refreshAllMemberCounts(projectId);
|
||||||
signale.info(`[SEGMENT-COUNT-WORKER] Updated counts for ${nonTrackedSegments.length} non-tracked segments`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
signale.error(`[SEGMENT-COUNT-WORKER] Failed to update counts for non-tracked segments:`, error);
|
signale.error(`[SEGMENT-COUNT-WORKER] Failed to update counts for non-tracked segments:`, error);
|
||||||
}
|
}
|
||||||
@@ -90,14 +79,10 @@ async function processProjectSegments(projectId: string, projectName?: string):
|
|||||||
async function processSegmentCountUpdate(job: Job<SegmentCountJobData>): Promise<void> {
|
async function processSegmentCountUpdate(job: Job<SegmentCountJobData>): Promise<void> {
|
||||||
const {projectId} = job.data;
|
const {projectId} = job.data;
|
||||||
|
|
||||||
signale.info(`[SEGMENT-COUNT-WORKER] Starting segment count update job ${job.id}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
// Process specific project
|
// Process specific project
|
||||||
signale.info(`[SEGMENT-COUNT-WORKER] Processing segments for project ${projectId}`);
|
|
||||||
await processProjectSegments(projectId);
|
await processProjectSegments(projectId);
|
||||||
signale.success(`[SEGMENT-COUNT-WORKER] Completed segments for project ${projectId}`);
|
|
||||||
} else {
|
} else {
|
||||||
// Process all active projects
|
// Process all active projects
|
||||||
const projects = await prisma.project.findMany({
|
const projects = await prisma.project.findMany({
|
||||||
@@ -105,7 +90,7 @@ async function processSegmentCountUpdate(job: Job<SegmentCountJobData>): Promise
|
|||||||
select: {id: true, name: true},
|
select: {id: true, name: true},
|
||||||
});
|
});
|
||||||
|
|
||||||
signale.info(`[SEGMENT-COUNT-WORKER] Found ${projects.length} active projects`);
|
signale.info(`[SEGMENT-COUNT-WORKER] Processing ${projects.length} active projects`);
|
||||||
|
|
||||||
// Process projects in batches to avoid overwhelming the database
|
// Process projects in batches to avoid overwhelming the database
|
||||||
const PROJECT_BATCH_SIZE = 10;
|
const PROJECT_BATCH_SIZE = 10;
|
||||||
@@ -115,9 +100,7 @@ async function processSegmentCountUpdate(job: Job<SegmentCountJobData>): Promise
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
batch.map(async project => {
|
batch.map(async project => {
|
||||||
try {
|
try {
|
||||||
signale.info(`[SEGMENT-COUNT-WORKER] Processing project ${project.name} (${project.id})`);
|
|
||||||
await processProjectSegments(project.id, project.name);
|
await processProjectSegments(project.id, project.name);
|
||||||
signale.success(`[SEGMENT-COUNT-WORKER] Completed project ${project.name}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
signale.error(`[SEGMENT-COUNT-WORKER] Failed to process project ${project.id}:`, error);
|
signale.error(`[SEGMENT-COUNT-WORKER] Failed to process project ${project.id}:`, error);
|
||||||
// Don't throw - continue with other projects
|
// Don't throw - continue with other projects
|
||||||
@@ -130,8 +113,6 @@ async function processSegmentCountUpdate(job: Job<SegmentCountJobData>): Promise
|
|||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signale.success(`[SEGMENT-COUNT-WORKER] Completed all segment updates`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
signale.error(`[SEGMENT-COUNT-WORKER] Error processing job ${job.id}:`, error);
|
signale.error(`[SEGMENT-COUNT-WORKER] Error processing job ${job.id}:`, error);
|
||||||
@@ -158,10 +139,6 @@ export function createSegmentCountWorker(): Worker {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', job => {
|
|
||||||
signale.success(`[SEGMENT-COUNT-WORKER] Job ${job.id} completed`);
|
|
||||||
});
|
|
||||||
|
|
||||||
worker.on('failed', (job, error) => {
|
worker.on('failed', (job, error) => {
|
||||||
signale.error(`[SEGMENT-COUNT-WORKER] Job ${job?.id} failed:`, error);
|
signale.error(`[SEGMENT-COUNT-WORKER] Job ${job?.id} failed:`, error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ const SECURITY_THRESHOLDS = {
|
|||||||
COMPLAINT_7DAY_CRITICAL: 0.15,
|
COMPLAINT_7DAY_CRITICAL: 0.15,
|
||||||
COMPLAINT_ALLTIME_WARNING: 0.03,
|
COMPLAINT_ALLTIME_WARNING: 0.03,
|
||||||
COMPLAINT_ALLTIME_CRITICAL: 0.12,
|
COMPLAINT_ALLTIME_CRITICAL: 0.12,
|
||||||
|
|
||||||
|
// Minimum absolute counts (prevents small sample size false positives)
|
||||||
|
// Both percentage AND absolute count must be exceeded to trigger
|
||||||
|
MIN_BOUNCES_FOR_CRITICAL: 10,
|
||||||
|
MIN_BOUNCES_FOR_WARNING: 5,
|
||||||
|
MIN_COMPLAINTS_FOR_CRITICAL: 5,
|
||||||
|
MIN_COMPLAINTS_FOR_WARNING: 3,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
interface RateData {
|
interface RateData {
|
||||||
@@ -280,51 +287,77 @@ export class SecurityService {
|
|||||||
|
|
||||||
// Check 7-day bounce rate (only if 7-day volume is sufficient)
|
// Check 7-day bounce rate (only if 7-day volume is sufficient)
|
||||||
if (hasMinimumVolume7Day) {
|
if (hasMinimumVolume7Day) {
|
||||||
if (sevenDay.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_7DAY_CRITICAL) {
|
// Critical: requires BOTH rate AND absolute count thresholds
|
||||||
|
if (
|
||||||
|
sevenDay.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_7DAY_CRITICAL &&
|
||||||
|
sevenDay.bounces >= SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_CRITICAL
|
||||||
|
) {
|
||||||
violations.push(
|
violations.push(
|
||||||
`7-day bounce rate (${sevenDay.bounceRate.toFixed(2)}%) exceeds critical threshold (${SECURITY_THRESHOLDS.BOUNCE_7DAY_CRITICAL}%)`,
|
`7-day bounce rate (${sevenDay.bounceRate.toFixed(2)}%, ${sevenDay.bounces} bounces) exceeds critical threshold (${SECURITY_THRESHOLDS.BOUNCE_7DAY_CRITICAL}%, ${SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_CRITICAL} minimum)`,
|
||||||
);
|
);
|
||||||
} else if (sevenDay.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_7DAY_WARNING) {
|
} else if (
|
||||||
|
sevenDay.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_7DAY_WARNING &&
|
||||||
|
sevenDay.bounces >= SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_WARNING
|
||||||
|
) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`7-day bounce rate (${sevenDay.bounceRate.toFixed(2)}%) exceeds warning threshold (${SECURITY_THRESHOLDS.BOUNCE_7DAY_WARNING}%)`,
|
`7-day bounce rate (${sevenDay.bounceRate.toFixed(2)}%, ${sevenDay.bounces} bounces) exceeds warning threshold (${SECURITY_THRESHOLDS.BOUNCE_7DAY_WARNING}%, ${SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_WARNING} minimum)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check 7-day complaint rate (only if 7-day volume is sufficient)
|
// Check 7-day complaint rate (only if 7-day volume is sufficient)
|
||||||
if (hasMinimumVolume7Day) {
|
if (hasMinimumVolume7Day) {
|
||||||
if (sevenDay.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_7DAY_CRITICAL) {
|
// Critical: requires BOTH rate AND absolute count thresholds
|
||||||
|
if (
|
||||||
|
sevenDay.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_7DAY_CRITICAL &&
|
||||||
|
sevenDay.complaints >= SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_CRITICAL
|
||||||
|
) {
|
||||||
violations.push(
|
violations.push(
|
||||||
`7-day complaint rate (${sevenDay.complaintRate.toFixed(3)}%) exceeds critical threshold (${SECURITY_THRESHOLDS.COMPLAINT_7DAY_CRITICAL}%)`,
|
`7-day complaint rate (${sevenDay.complaintRate.toFixed(3)}%, ${sevenDay.complaints} complaints) exceeds critical threshold (${SECURITY_THRESHOLDS.COMPLAINT_7DAY_CRITICAL}%, ${SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_CRITICAL} minimum)`,
|
||||||
);
|
);
|
||||||
} else if (sevenDay.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_7DAY_WARNING) {
|
} else if (
|
||||||
|
sevenDay.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_7DAY_WARNING &&
|
||||||
|
sevenDay.complaints >= SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_WARNING
|
||||||
|
) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`7-day complaint rate (${sevenDay.complaintRate.toFixed(3)}%) exceeds warning threshold (${SECURITY_THRESHOLDS.COMPLAINT_7DAY_WARNING}%)`,
|
`7-day complaint rate (${sevenDay.complaintRate.toFixed(3)}%, ${sevenDay.complaints} complaints) exceeds warning threshold (${SECURITY_THRESHOLDS.COMPLAINT_7DAY_WARNING}%, ${SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_WARNING} minimum)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all-time rates (only if all-time volume is sufficient)
|
// Check all-time rates (only if all-time volume is sufficient)
|
||||||
if (hasMinimumVolumeAllTime) {
|
if (hasMinimumVolumeAllTime) {
|
||||||
// Check all-time bounce rate
|
// Check all-time bounce rate - requires BOTH rate AND absolute count
|
||||||
if (allTime.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_ALLTIME_CRITICAL) {
|
if (
|
||||||
|
allTime.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_ALLTIME_CRITICAL &&
|
||||||
|
allTime.bounces >= SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_CRITICAL
|
||||||
|
) {
|
||||||
violations.push(
|
violations.push(
|
||||||
`All-time bounce rate (${allTime.bounceRate.toFixed(2)}%) exceeds critical threshold (${SECURITY_THRESHOLDS.BOUNCE_ALLTIME_CRITICAL}%)`,
|
`All-time bounce rate (${allTime.bounceRate.toFixed(2)}%, ${allTime.bounces} bounces) exceeds critical threshold (${SECURITY_THRESHOLDS.BOUNCE_ALLTIME_CRITICAL}%, ${SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_CRITICAL} minimum)`,
|
||||||
);
|
);
|
||||||
} else if (allTime.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_ALLTIME_WARNING) {
|
} else if (
|
||||||
|
allTime.bounceRate >= SECURITY_THRESHOLDS.BOUNCE_ALLTIME_WARNING &&
|
||||||
|
allTime.bounces >= SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_WARNING
|
||||||
|
) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`All-time bounce rate (${allTime.bounceRate.toFixed(2)}%) exceeds warning threshold (${SECURITY_THRESHOLDS.BOUNCE_ALLTIME_WARNING}%)`,
|
`All-time bounce rate (${allTime.bounceRate.toFixed(2)}%, ${allTime.bounces} bounces) exceeds warning threshold (${SECURITY_THRESHOLDS.BOUNCE_ALLTIME_WARNING}%, ${SECURITY_THRESHOLDS.MIN_BOUNCES_FOR_WARNING} minimum)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all-time complaint rate
|
// Check all-time complaint rate - requires BOTH rate AND absolute count
|
||||||
if (allTime.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_CRITICAL) {
|
if (
|
||||||
|
allTime.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_CRITICAL &&
|
||||||
|
allTime.complaints >= SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_CRITICAL
|
||||||
|
) {
|
||||||
violations.push(
|
violations.push(
|
||||||
`All-time complaint rate (${allTime.complaintRate.toFixed(3)}%) exceeds critical threshold (${SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_CRITICAL}%)`,
|
`All-time complaint rate (${allTime.complaintRate.toFixed(3)}%, ${allTime.complaints} complaints) exceeds critical threshold (${SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_CRITICAL}%, ${SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_CRITICAL} minimum)`,
|
||||||
);
|
);
|
||||||
} else if (allTime.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_WARNING) {
|
} else if (
|
||||||
|
allTime.complaintRate >= SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_WARNING &&
|
||||||
|
allTime.complaints >= SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_WARNING
|
||||||
|
) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`All-time complaint rate (${allTime.complaintRate.toFixed(3)}%) exceeds warning threshold (${SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_WARNING}%)`,
|
`All-time complaint rate (${allTime.complaintRate.toFixed(3)}%, ${allTime.complaints} complaints) exceeds warning threshold (${SECURITY_THRESHOLDS.COMPLAINT_ALLTIME_WARNING}%, ${SECURITY_THRESHOLDS.MIN_COMPLAINTS_FOR_WARNING} minimum)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
|
|||||||
setIsLoadingMore(true);
|
setIsLoadingMore(true);
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setActivities([]);
|
// Don't clear activities immediately - we'll do a smart merge
|
||||||
|
// This preserves React component instances and their local state
|
||||||
setNextCursor(undefined);
|
setNextCursor(undefined);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
}
|
}
|
||||||
@@ -58,11 +59,25 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
|
|||||||
const result = await network.fetch<CursorPaginatedResponse<Activity>>('GET', `/activity?${params.toString()}`);
|
const result = await network.fetch<CursorPaginatedResponse<Activity>>('GET', `/activity?${params.toString()}`);
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
// Append to existing activities
|
// Append to existing activities (pagination)
|
||||||
setActivities(prev => [...prev, ...result.data]);
|
setActivities(prev => [...prev, ...result.data]);
|
||||||
} else {
|
} else {
|
||||||
// Replace activities
|
// Smart merge: preserve existing activity objects by ID to maintain component state
|
||||||
setActivities(result.data);
|
setActivities(prev => {
|
||||||
|
// If no previous activities, just use new data
|
||||||
|
if (prev.length === 0) {
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of existing activities by ID for fast lookup
|
||||||
|
const existingMap = new Map(prev.map(activity => [activity.id, activity]));
|
||||||
|
|
||||||
|
// For each new activity, reuse existing object if ID matches (preserves React component instances)
|
||||||
|
// Otherwise use new object. This maintains correct ordering while preserving component state.
|
||||||
|
return result.data.map(newActivity =>
|
||||||
|
existingMap.get(newActivity.id) ?? newActivity
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setNextCursor(result.cursor);
|
setNextCursor(result.cursor);
|
||||||
@@ -95,7 +110,21 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
|
|||||||
|
|
||||||
const result = await network.fetch<{activities: Activity[]}>('GET', `/activity/upcoming?${params.toString()}`);
|
const result = await network.fetch<{activities: Activity[]}>('GET', `/activity/upcoming?${params.toString()}`);
|
||||||
|
|
||||||
setUpcomingActivities(result.activities);
|
// Smart merge: preserve existing activity objects by ID to maintain component state
|
||||||
|
setUpcomingActivities(prev => {
|
||||||
|
// If no previous activities, just use new data
|
||||||
|
if (prev.length === 0) {
|
||||||
|
return result.activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of existing activities by ID for fast lookup
|
||||||
|
const existingMap = new Map(prev.map(activity => [activity.id, activity]));
|
||||||
|
|
||||||
|
// For each new activity, reuse existing object if ID matches
|
||||||
|
return result.activities.map(newActivity =>
|
||||||
|
existingMap.get(newActivity.id) ?? newActivity
|
||||||
|
);
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching upcoming activities:', err);
|
console.error('Error fetching upcoming activities:', err);
|
||||||
// Don't set error state for upcoming - just fail silently
|
// Don't set error state for upcoming - just fail silently
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, {useEffect} from 'react';
|
|||||||
import {Toaster} from 'sonner';
|
import {Toaster} from 'sonner';
|
||||||
import {SWRConfig} from 'swr';
|
import {SWRConfig} from 'swr';
|
||||||
import {DefaultSeo} from 'next-seo';
|
import {DefaultSeo} from 'next-seo';
|
||||||
|
import {NuqsAdapter} from 'nuqs/adapters/next/pages';
|
||||||
import {Loader} from '@plunk/ui';
|
import {Loader} from '@plunk/ui';
|
||||||
import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
|
import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
|
||||||
import {useProjects} from '../lib/hooks/useProject';
|
import {useProjects} from '../lib/hooks/useProject';
|
||||||
@@ -103,6 +104,7 @@ function ProjectGuard({children}: {children: React.ReactNode}) {
|
|||||||
*/
|
*/
|
||||||
export default function WithProviders(props: AppProps) {
|
export default function WithProviders(props: AppProps) {
|
||||||
return (
|
return (
|
||||||
|
<NuqsAdapter>
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
value={{
|
value={{
|
||||||
fetcher: (url: string) => network.fetch('GET', url),
|
fetcher: (url: string) => network.fetch('GET', url),
|
||||||
@@ -114,6 +116,7 @@ export default function WithProviders(props: AppProps) {
|
|||||||
<Root {...props} />
|
<Root {...props} />
|
||||||
</ActiveProjectProvider>
|
</ActiveProjectProvider>
|
||||||
</SWRConfig>
|
</SWRConfig>
|
||||||
|
</NuqsAdapter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {DashboardLayout} from '../../components/DashboardLayout';
|
|||||||
import {ActivityFeed} from '../../components/ActivityFeed';
|
import {ActivityFeed} from '../../components/ActivityFeed';
|
||||||
import {Eye, MousePointerClick, Send, Zap} from 'lucide-react';
|
import {Eye, MousePointerClick, Send, Zap} from 'lucide-react';
|
||||||
import {NextSeo} from 'next-seo';
|
import {NextSeo} from 'next-seo';
|
||||||
import {useState} from 'react';
|
import {useQueryState, parseAsString} from 'nuqs';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
interface ActivityStats {
|
interface ActivityStats {
|
||||||
@@ -28,8 +28,8 @@ interface ActivityStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ActivityPage() {
|
export default function ActivityPage() {
|
||||||
const [typeFilter, setTypeFilter] = useState<string>('ALL');
|
const [typeFilter, setTypeFilter] = useQueryState('type', parseAsString.withDefault('ALL'));
|
||||||
const [dateRange, setDateRange] = useState<string>('30');
|
const [dateRange, setDateRange] = useQueryState('days', parseAsString.withDefault('30'));
|
||||||
|
|
||||||
// Fetch activity stats
|
// Fetch activity stats
|
||||||
const {data: stats} = useSWR<ActivityStats>(`/activity/stats`, {
|
const {data: stats} = useSWR<ActivityStats>(`/activity/stats`, {
|
||||||
|
|||||||
@@ -44,3 +44,31 @@ Certain keys are reserved by the system and automatically set by Plunk:
|
|||||||
| Key | Description |
|
| Key | Description |
|
||||||
|-----|-------------|
|
|-----|-------------|
|
||||||
| locale | The contact's preferred locale in ISO 639 (e.g. 'en', 'fr', 'es'). Specifying the locale field on a contact will override the project-wide locale for contact-facing pages and email footers |
|
| locale | The contact's preferred locale in ISO 639 (e.g. 'en', 'fr', 'es'). Specifying the locale field on a contact will override the project-wide locale for contact-facing pages and email footers |
|
||||||
|
|
||||||
|
## Subscription State
|
||||||
|
|
||||||
|
Every contact has a `subscribed` field that determines which types of emails they will receive. A newly created contact is subscribed by default.
|
||||||
|
|
||||||
|
### How contacts become unsubscribed
|
||||||
|
|
||||||
|
A contact can become unsubscribed in several ways:
|
||||||
|
- **Manually** through the dashboard or via the API
|
||||||
|
- **Self-service** by clicking the unsubscribe link in an email
|
||||||
|
- **Automatically** when an email to the contact bounces or results in a complaint
|
||||||
|
|
||||||
|
### Emails by subscription state
|
||||||
|
|
||||||
|
The subscription state controls whether a contact receives marketing emails. Transactional emails are always delivered regardless of subscription state.
|
||||||
|
|
||||||
|
| Email type | Subscribed | Unsubscribed |
|
||||||
|
|---|---|---|
|
||||||
|
| **Transactional** (via [/v1/send](/api-reference/public-api/sendTransactionalEmail)) | Delivered | Delivered |
|
||||||
|
| **Campaigns** | Delivered | Not delivered |
|
||||||
|
| **Automations** (transactional template) | Delivered | Delivered |
|
||||||
|
| **Automations** (marketing template) | Delivered | Not delivered |
|
||||||
|
|
||||||
|
<Callout
|
||||||
|
title="Transactional emails and marketing templates"
|
||||||
|
variant="warn">
|
||||||
|
Even when using the transactional API endpoint (`/v1/send`), you cannot send a marketing template to an unsubscribed contact. Use a transactional template instead if the email must reach unsubscribed contacts.
|
||||||
|
</Callout>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"pages": ["list-hygiene", "verifying-domains", "tracking", "api-keys", "localization"]
|
"pages": ["list-hygiene", "verifying-domains", "tracking", "api-keys", "localization", "webhooks"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
title: Webhooks
|
||||||
|
description: Send real-time event data from Plunk to your own application using webhooks
|
||||||
|
icon: Webhook
|
||||||
|
---
|
||||||
|
|
||||||
|
Plunk can send real-time HTTP requests to your application when specific events occur, such as email bounces, spam complaints, or custom events. This is done by creating a [workflow](/concepts/workflows) that uses the **Webhook** step to forward event data to your own endpoint.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Webhooks in Plunk are powered by the workflow system. The basic flow is:
|
||||||
|
|
||||||
|
1. An event occurs in Plunk (e.g. an email bounces, a contact subscribes, or a custom event is tracked)
|
||||||
|
2. A workflow is triggered by that event
|
||||||
|
3. The workflow executes a **Webhook** step, sending an HTTP request to your URL with relevant data
|
||||||
|
|
||||||
|
This means you can receive notifications for any event Plunk tracks, including both system events and your own custom events.
|
||||||
|
|
||||||
|
## Internal events
|
||||||
|
|
||||||
|
Plunk automatically tracks a set of internal events that you can use as workflow triggers. These events cannot be manually tracked via the API — they are generated by the system.
|
||||||
|
|
||||||
|
### Email events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `email.sent` | An email was successfully sent |
|
||||||
|
| `email.delivery` | An email was delivered to the recipient |
|
||||||
|
| `email.open` | A contact opened an email for the first time |
|
||||||
|
| `email.click` | A contact clicked a link in an email for the first time |
|
||||||
|
| `email.bounce` | An email bounced (hard or soft bounce) |
|
||||||
|
| `email.complaint` | A contact marked an email as spam |
|
||||||
|
|
||||||
|
### Contact events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `contact.subscribed` | A contact's subscription status changed to subscribed |
|
||||||
|
| `contact.unsubscribed` | A contact's subscription status changed to unsubscribed |
|
||||||
|
|
||||||
|
### Segment events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `segment.<name>.entry` | A contact entered a segment |
|
||||||
|
| `segment.<name>.exit` | A contact exited a segment |
|
||||||
|
|
||||||
|
<Callout
|
||||||
|
title="Segment event names"
|
||||||
|
variant="idea">
|
||||||
|
Segment events use a slugified version of the segment name. For example, a segment called "VIP Users" would produce the events `segment.vip-users.entry` and `segment.vip-users.exit`.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Setting up a webhook
|
||||||
|
|
||||||
|
<div className='fd-steps [&_h3]:fd-step'>
|
||||||
|
|
||||||
|
### Create the workflow
|
||||||
|
|
||||||
|
Navigate to the **Workflows** section in the dashboard and create a new workflow. Choose the event you want to listen for as the trigger. For example, to receive notifications when an email bounces, use `email.bounce` as the trigger event.
|
||||||
|
|
||||||
|
### Add a Webhook step
|
||||||
|
|
||||||
|
After the trigger, add a **Webhook** step and configure it:
|
||||||
|
|
||||||
|
- **URL**: The endpoint on your server that will receive the webhook (e.g. `https://api.example.com/webhooks/plunk`)
|
||||||
|
- **Method**: The HTTP method to use. Defaults to `POST`, which is recommended for most use cases.
|
||||||
|
- **Headers** (optional): Custom headers to include in the request, provided as JSON. This is useful for authentication.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Authorization": "Bearer your-secret-token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable the workflow
|
||||||
|
|
||||||
|
Once configured, enable the workflow. It will start sending webhook requests whenever the trigger event occurs.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Webhook payload
|
||||||
|
|
||||||
|
When using the default payload (no custom body configured), Plunk sends a JSON request with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contact": {
|
||||||
|
"email": "[email protected]",
|
||||||
|
"subscribed": true,
|
||||||
|
"data": {
|
||||||
|
"name": "John",
|
||||||
|
"plan": "pro"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"id": "wf_abc123",
|
||||||
|
"name": "Bounce Notifications"
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"id": "exec_xyz789",
|
||||||
|
"startedAt": "2025-01-15T10:30:00.000Z"
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"subject": "Welcome to Plunk",
|
||||||
|
"from": "[email protected]",
|
||||||
|
"bounceType": "Permanent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `event` field contains the data associated with the event that triggered the workflow. The exact contents depend on the event type.
|
||||||
|
|
||||||
|
### Event data by type
|
||||||
|
|
||||||
|
The `event` field varies depending on which event triggered the workflow:
|
||||||
|
|
||||||
|
| Event | Fields in `event` |
|
||||||
|
|-------|-------------------|
|
||||||
|
| `email.sent` | `subject`, `from`, `messageId`, `templateId`, `campaignId`, `sourceType` |
|
||||||
|
| `email.open` | `subject`, `from`, `openedAt`, `isFirstOpen` |
|
||||||
|
| `email.click` | `subject`, `from`, `clickedAt`, `clicks`, `isFirstClick` |
|
||||||
|
| `email.bounce` | `subject`, `from`, `bounceType`, `bouncedAt` |
|
||||||
|
| `email.complaint` | `subject`, `from`, `complainedAt` |
|
||||||
|
| Custom events | Whatever data you passed when tracking the event |
|
||||||
|
|
||||||
|
## Common use cases
|
||||||
|
|
||||||
|
### Bounce and complaint monitoring
|
||||||
|
|
||||||
|
Create a workflow triggered by `email.bounce` or `email.complaint` to forward these events to your application. This allows you to keep your own database in sync with Plunk's contact statuses.
|
||||||
|
|
||||||
|
You can use additional workflow steps before the webhook to add logic:
|
||||||
|
|
||||||
|
- **Condition**: Only send the webhook for hard bounces by checking the `bounceType` field
|
||||||
|
- **Delay**: Add a short delay to batch-process related events
|
||||||
|
- **Update Contact**: Mark the contact with metadata before sending the webhook
|
||||||
|
|
||||||
|
### Syncing unsubscribes
|
||||||
|
|
||||||
|
Trigger a workflow on `contact.unsubscribed` to notify your application when a contact opts out. This is useful for keeping subscription status synchronized across multiple systems.
|
||||||
|
|
||||||
|
### Custom event forwarding
|
||||||
|
|
||||||
|
If you track custom events in Plunk (e.g. `user.signup`, `order.completed`), you can forward those same events to other services via webhooks. This turns Plunk into an event router — track once, distribute to multiple endpoints.
|
||||||
|
|
||||||
|
## Adding conditions and delays
|
||||||
|
|
||||||
|
Since webhooks are part of the workflow system, you can combine them with other step types for more advanced setups:
|
||||||
|
|
||||||
|
- Use a **Condition** step to only fire the webhook when certain criteria are met (e.g. only notify for contacts on a specific plan)
|
||||||
|
- Use a **Wait for Event** step to wait for a follow-up event before sending the webhook (e.g. wait to see if a bounced contact re-subscribes)
|
||||||
|
- Use a **Delay** step to add a time buffer before the webhook fires
|
||||||
@@ -7,6 +7,9 @@ import deTranslations from './locales/de.json' with {type: 'json'};
|
|||||||
import hiTranslations from './locales/hi.json' with {type: 'json'};
|
import hiTranslations from './locales/hi.json' with {type: 'json'};
|
||||||
import ptTranslations from './locales/pt.json' with {type: 'json'};
|
import ptTranslations from './locales/pt.json' with {type: 'json'};
|
||||||
import bgTranslations from './locales/bg.json' with {type: 'json'};
|
import bgTranslations from './locales/bg.json' with {type: 'json'};
|
||||||
|
import csTranslations from './locales/cs.json' with {type: 'json'};
|
||||||
|
import plTranslations from './locales/pl.json' with {type: 'json'};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SUPPORTED_LANGUAGES,
|
SUPPORTED_LANGUAGES,
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
@@ -38,6 +41,8 @@ const translationsMap: Record<string, Translations> = {
|
|||||||
hi: hiTranslations,
|
hi: hiTranslations,
|
||||||
pt: ptTranslations,
|
pt: ptTranslations,
|
||||||
bg: bgTranslations,
|
bg: bgTranslations,
|
||||||
|
cs: csTranslations,
|
||||||
|
pl: plTranslations,
|
||||||
};
|
};
|
||||||
|
|
||||||
// In-memory cache for loaded translations
|
// In-memory cache for loaded translations
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const SUPPORTED_LANGUAGES: Language[] = [
|
|||||||
{code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪'},
|
{code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪'},
|
||||||
{code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷'},
|
{code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷'},
|
||||||
{code: 'bg', name: 'Bulgarian', nativeName: 'Български', flag: '🇧🇬'},
|
{code: 'bg', name: 'Bulgarian', nativeName: 'Български', flag: '🇧🇬'},
|
||||||
|
{code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿'},
|
||||||
|
{code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱'},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_LANGUAGE = 'en';
|
export const DEFAULT_LANGUAGE = 'en';
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"pages": {
|
||||||
|
"unsubscribe": {
|
||||||
|
"title": "Odhlášení z odběru",
|
||||||
|
"description": "Mrzí nás, že odcházíte. Opravdu chcete odhlásit {email} z odběru e-mailů?",
|
||||||
|
"button": "Odhlásit se",
|
||||||
|
"buttonLoading": "Odhlašování...",
|
||||||
|
"managePreferences": "Spravovat předvolby",
|
||||||
|
"successTitle": "Odběr byl zrušen",
|
||||||
|
"successDescription": "E-mail {email} byl odhlášen z odběru. Už vám nebudeme zasílat žádné e-maily.",
|
||||||
|
"changedMind": "Změnili jste názor?",
|
||||||
|
"subscribeAgain": "Přihlásit se znovu"
|
||||||
|
},
|
||||||
|
"subscribe": {
|
||||||
|
"title": "Přihlášení k odběru",
|
||||||
|
"description": "Chcete přihlásit {email} k odběru e-mailů?",
|
||||||
|
"button": "Přihlásit se k odběru",
|
||||||
|
"buttonLoading": "Přihlašování...",
|
||||||
|
"successTitle": "Odběr byl aktivován!",
|
||||||
|
"successDescription": "E-mail {email} byl přihlášen k odběru e-mailů."
|
||||||
|
},
|
||||||
|
"manage": {
|
||||||
|
"title": "Správa předvoleb",
|
||||||
|
"description": "Správa e-mailových předvoleb pro {email}",
|
||||||
|
"subscriptionLabel": "Odběr e-mailů",
|
||||||
|
"subscribedStatus": "Aktuálně jste přihlášeni k odběru e-mailů",
|
||||||
|
"unsubscribedStatus": "Aktuálně jste odhlášeni z odběru e-mailů",
|
||||||
|
"subscribedSuccess": "Odběr byl úspěšně aktivován!",
|
||||||
|
"unsubscribedSuccess": "Odběr byl úspěšně zrušen!",
|
||||||
|
"unsubscribeCompletely": "Úplně se odhlásit z odběru",
|
||||||
|
"subscribeToEmails": "Přihlásit se k odběru e-mailů",
|
||||||
|
"disclaimer": "Na této stránce můžete spravovat své e-mailové předvolby. Stav odběru se aktualizuje v reálném čase."
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "Načítání...",
|
||||||
|
"error": "Chyba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"footer": {
|
||||||
|
"unsubscribeText": "Tento e-mail jste obdrželi, protože jste souhlasili se zasíláním e-mailů od {projectName}. Pokud si již nepřejete dostávat tyto e-maily, můžete",
|
||||||
|
"updatePreferences": "změnit své předvolby"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"pages": {
|
||||||
|
"unsubscribe": {
|
||||||
|
"title": "Wypisz się",
|
||||||
|
"description": "Przykro nam, że odchodzisz. Czy na pewno chcesz wypisać {email} z otrzymywania wiadomości?",
|
||||||
|
"button": "Wypisz się",
|
||||||
|
"buttonLoading": "Wypisywanie...",
|
||||||
|
"managePreferences": "Zarządzaj preferencjami zamiast tego",
|
||||||
|
"successTitle": "Wypisano",
|
||||||
|
"successDescription": "{email} został wypisany. Nie będziesz już otrzymywać od nas wiadomości.",
|
||||||
|
"changedMind": "Zmieniłeś(-aś) zdanie?",
|
||||||
|
"subscribeAgain": "Zapisz się ponownie"
|
||||||
|
},
|
||||||
|
"subscribe": {
|
||||||
|
"title": "Zapisz się na aktualizacje",
|
||||||
|
"description": "Czy chcesz zapisać {email} do otrzymywania wiadomości?",
|
||||||
|
"button": "Zapisz się",
|
||||||
|
"buttonLoading": "Zapisywanie...",
|
||||||
|
"successTitle": "Zapisano!",
|
||||||
|
"successDescription": "{email} jest teraz zapisany do otrzymywania od nas wiadomości."
|
||||||
|
},
|
||||||
|
"manage": {
|
||||||
|
"title": "Zarządzaj preferencjami",
|
||||||
|
"description": "Zarządzaj preferencjami e-mail dla adresu {email}",
|
||||||
|
"subscriptionLabel": "Subskrypcja e-mail",
|
||||||
|
"subscribedStatus": "Obecnie jesteś zapisany do otrzymywania wiadomości",
|
||||||
|
"unsubscribedStatus": "Obecnie jesteś wypisany z otrzymywania wiadomości",
|
||||||
|
"subscribedSuccess": "Pomyślnie zapisano!",
|
||||||
|
"unsubscribedSuccess": "Pomyślnie wypisano!",
|
||||||
|
"unsubscribeCompletely": "Wypisz się całkowicie",
|
||||||
|
"subscribeToEmails": "Zapisz się na e-maile",
|
||||||
|
"disclaimer": "Ta strona pozwala zarządzać preferencjami e-mail. Status subskrypcji jest aktualizowany w czasie rzeczywistym."
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"loading": "Ładowanie...",
|
||||||
|
"error": "Błąd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"footer": {
|
||||||
|
"unsubscribeText": "Otrzymujesz tę wiadomość, ponieważ wyraziłeś(-aś) zgodę na otrzymywanie wiadomości od {projectName}. Jeśli nie chcesz już otrzymywać takich wiadomości, prosimy",
|
||||||
|
"updatePreferences": "zaktualizuj swoje preferencje"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2821,10 +2821,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/env@npm:16.1.5":
|
"@next/env@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/env@npm:16.1.5"
|
resolution: "@next/env@npm:16.1.6"
|
||||||
checksum: 10c0/9d6442bee75386593d5da6e952146cf3c4338202e68a0ba9464d70c24ab7abb0c7e4ecd0ab661ca211e544fe7d6bd075270379bee3e97c5da3b6d082f95a04cd
|
checksum: 10c0/ed7023edb94b9b2e5da3f9c99d08b614da9757c1edd0ecec792fce4d336b4f0c64db1a84955e07cfbd848b9e61c4118fff28f4098cd7b0a7f97814a90565ebe6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -2851,9 +2851,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@npm:16.1.5":
|
"@next/swc-darwin-arm64@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-darwin-arm64@npm:16.1.5"
|
resolution: "@next/swc-darwin-arm64@npm:16.1.6"
|
||||||
conditions: os=darwin & cpu=arm64
|
conditions: os=darwin & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2865,9 +2865,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-darwin-x64@npm:16.1.5":
|
"@next/swc-darwin-x64@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-darwin-x64@npm:16.1.5"
|
resolution: "@next/swc-darwin-x64@npm:16.1.6"
|
||||||
conditions: os=darwin & cpu=x64
|
conditions: os=darwin & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2879,9 +2879,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@npm:16.1.5":
|
"@next/swc-linux-arm64-gnu@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-linux-arm64-gnu@npm:16.1.5"
|
resolution: "@next/swc-linux-arm64-gnu@npm:16.1.6"
|
||||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2893,9 +2893,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@npm:16.1.5":
|
"@next/swc-linux-arm64-musl@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-linux-arm64-musl@npm:16.1.5"
|
resolution: "@next/swc-linux-arm64-musl@npm:16.1.6"
|
||||||
conditions: os=linux & cpu=arm64 & libc=musl
|
conditions: os=linux & cpu=arm64 & libc=musl
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2907,9 +2907,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@npm:16.1.5":
|
"@next/swc-linux-x64-gnu@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-linux-x64-gnu@npm:16.1.5"
|
resolution: "@next/swc-linux-x64-gnu@npm:16.1.6"
|
||||||
conditions: os=linux & cpu=x64 & libc=glibc
|
conditions: os=linux & cpu=x64 & libc=glibc
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2921,9 +2921,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@npm:16.1.5":
|
"@next/swc-linux-x64-musl@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-linux-x64-musl@npm:16.1.5"
|
resolution: "@next/swc-linux-x64-musl@npm:16.1.6"
|
||||||
conditions: os=linux & cpu=x64 & libc=musl
|
conditions: os=linux & cpu=x64 & libc=musl
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2935,9 +2935,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@npm:16.1.5":
|
"@next/swc-win32-arm64-msvc@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-win32-arm64-msvc@npm:16.1.5"
|
resolution: "@next/swc-win32-arm64-msvc@npm:16.1.6"
|
||||||
conditions: os=win32 & cpu=arm64
|
conditions: os=win32 & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -2949,9 +2949,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@npm:16.1.5":
|
"@next/swc-win32-x64-msvc@npm:16.1.6":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "@next/swc-win32-x64-msvc@npm:16.1.5"
|
resolution: "@next/swc-win32-x64-msvc@npm:16.1.6"
|
||||||
conditions: os=win32 & cpu=x64
|
conditions: os=win32 & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -8317,7 +8317,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.26.3":
|
"browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.28.1":
|
||||||
version: 4.28.1
|
version: 4.28.1
|
||||||
resolution: "browserslist@npm:4.28.1"
|
resolution: "browserslist@npm:4.28.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9820,7 +9820,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"enhanced-resolve@npm:^5.17.3, enhanced-resolve@npm:^5.18.3":
|
"enhanced-resolve@npm:^5.18.3":
|
||||||
version: 5.18.4
|
version: 5.18.4
|
||||||
resolution: "enhanced-resolve@npm:5.18.4"
|
resolution: "enhanced-resolve@npm:5.18.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9830,6 +9830,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"enhanced-resolve@npm:^5.19.0":
|
||||||
|
version: 5.19.0
|
||||||
|
resolution: "enhanced-resolve@npm:5.19.0"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: "npm:^4.2.4"
|
||||||
|
tapable: "npm:^2.3.0"
|
||||||
|
checksum: 10c0/966b1dffb82d5f6a4d6a86e904e812104a999066aa29f9223040aaa751e7c453b462a3f5ef91f8bd4408131ff6f7f90651dd1c804bdcb7944e2099a9c2e45ee2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"entities@npm:^2.0.0":
|
"entities@npm:^2.0.0":
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
resolution: "entities@npm:2.2.0"
|
resolution: "entities@npm:2.2.0"
|
||||||
@@ -9988,13 +9998,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.7.0":
|
"es-module-lexer@npm:^1.7.0":
|
||||||
version: 1.7.0
|
version: 1.7.0
|
||||||
resolution: "es-module-lexer@npm:1.7.0"
|
resolution: "es-module-lexer@npm:1.7.0"
|
||||||
checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
|
checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"es-module-lexer@npm:^2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "es-module-lexer@npm:2.0.0"
|
||||||
|
checksum: 10c0/ae78dbbd43035a4b972c46cfb6877e374ea290adfc62bc2f5a083fea242c0b2baaab25c5886af86be55f092f4a326741cb94334cd3c478c383fdc8a9ec5ff817
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
|
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "es-object-atoms@npm:1.1.1"
|
resolution: "es-object-atoms@npm:1.1.1"
|
||||||
@@ -13405,8 +13422,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"markdown-it@npm:^14.0.0":
|
"markdown-it@npm:^14.0.0":
|
||||||
version: 14.1.0
|
version: 14.1.1
|
||||||
resolution: "markdown-it@npm:14.1.0"
|
resolution: "markdown-it@npm:14.1.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: "npm:^2.0.1"
|
argparse: "npm:^2.0.1"
|
||||||
entities: "npm:^4.4.0"
|
entities: "npm:^4.4.0"
|
||||||
@@ -13416,7 +13433,7 @@ __metadata:
|
|||||||
uc.micro: "npm:^2.1.0"
|
uc.micro: "npm:^2.1.0"
|
||||||
bin:
|
bin:
|
||||||
markdown-it: bin/markdown-it.mjs
|
markdown-it: bin/markdown-it.mjs
|
||||||
checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
|
checksum: 10c0/c67f2a4c8069a307c78d8c15104bbcb15a2c6b17f4c904364ca218ec2eccf76a397eba1ea05f5ac5de72c4b67fcf115d422d22df0bfb86a09b663f55b9478d4f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -14651,18 +14668,18 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"next@npm:^16.1.5":
|
"next@npm:^16.1.5":
|
||||||
version: 16.1.5
|
version: 16.1.6
|
||||||
resolution: "next@npm:16.1.5"
|
resolution: "next@npm:16.1.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env": "npm:16.1.5"
|
"@next/env": "npm:16.1.6"
|
||||||
"@next/swc-darwin-arm64": "npm:16.1.5"
|
"@next/swc-darwin-arm64": "npm:16.1.6"
|
||||||
"@next/swc-darwin-x64": "npm:16.1.5"
|
"@next/swc-darwin-x64": "npm:16.1.6"
|
||||||
"@next/swc-linux-arm64-gnu": "npm:16.1.5"
|
"@next/swc-linux-arm64-gnu": "npm:16.1.6"
|
||||||
"@next/swc-linux-arm64-musl": "npm:16.1.5"
|
"@next/swc-linux-arm64-musl": "npm:16.1.6"
|
||||||
"@next/swc-linux-x64-gnu": "npm:16.1.5"
|
"@next/swc-linux-x64-gnu": "npm:16.1.6"
|
||||||
"@next/swc-linux-x64-musl": "npm:16.1.5"
|
"@next/swc-linux-x64-musl": "npm:16.1.6"
|
||||||
"@next/swc-win32-arm64-msvc": "npm:16.1.5"
|
"@next/swc-win32-arm64-msvc": "npm:16.1.6"
|
||||||
"@next/swc-win32-x64-msvc": "npm:16.1.5"
|
"@next/swc-win32-x64-msvc": "npm:16.1.6"
|
||||||
"@swc/helpers": "npm:0.5.15"
|
"@swc/helpers": "npm:0.5.15"
|
||||||
baseline-browser-mapping: "npm:^2.8.3"
|
baseline-browser-mapping: "npm:^2.8.3"
|
||||||
caniuse-lite: "npm:^1.0.30001579"
|
caniuse-lite: "npm:^1.0.30001579"
|
||||||
@@ -14706,7 +14723,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
next: dist/bin/next
|
next: dist/bin/next
|
||||||
checksum: 10c0/ff9f7dd0cae79f7ba2a64ac0d29ca77c94921dfc37b36899f4f997b948242f72585f4bb75829aaf8704d46c6f7d1a9796b8ff30688a3467c04c738b3a4247a9f
|
checksum: 10c0/543766bf879bb5a5d454dc18cb302953270a92efba1d01dd028ea83c64b69573ce7d6e6c3759ecbaabec0a84131b0237263c24d1ccd7c8a97205e776dcd34e0b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -15898,11 +15915,11 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.14.0, qs@npm:~6.14.0":
|
"qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.14.0, qs@npm:~6.14.0":
|
||||||
version: 6.14.1
|
version: 6.14.2
|
||||||
resolution: "qs@npm:6.14.1"
|
resolution: "qs@npm:6.14.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: "npm:^1.1.0"
|
side-channel: "npm:^1.1.0"
|
||||||
checksum: 10c0/0e3b22dc451f48ce5940cbbc7c7d9068d895074f8c969c0801ac15c1313d1859c4d738e46dc4da2f498f41a9ffd8c201bd9fb12df67799b827db94cc373d2613
|
checksum: 10c0/646110124476fc9acf3c80994c8c3a0600cbad06a4ede1c9e93341006e8426d64e85e048baf8f0c4995f0f1bf0f37d1f3acc5ec1455850b81978792969a60ef6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -17999,7 +18016,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"terser-webpack-plugin@npm:^5.3.11":
|
"terser-webpack-plugin@npm:^5.3.16":
|
||||||
version: 5.3.16
|
version: 5.3.16
|
||||||
resolution: "terser-webpack-plugin@npm:5.3.16"
|
resolution: "terser-webpack-plugin@npm:5.3.16"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -19017,13 +19034,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"watchpack@npm:^2.4.4":
|
"watchpack@npm:^2.5.1":
|
||||||
version: 2.4.4
|
version: 2.5.1
|
||||||
resolution: "watchpack@npm:2.4.4"
|
resolution: "watchpack@npm:2.5.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
glob-to-regexp: "npm:^0.4.1"
|
glob-to-regexp: "npm:^0.4.1"
|
||||||
graceful-fs: "npm:^4.1.2"
|
graceful-fs: "npm:^4.1.2"
|
||||||
checksum: 10c0/6c0901f75ce245d33991225af915eea1c5ae4ba087f3aee2b70dd377d4cacb34bef02a48daf109da9d59b2d31ec6463d924a0d72f8618ae1643dd07b95de5275
|
checksum: 10c0/dffbb483d1f61be90dc570630a1eb308581e2227d507d783b1d94a57ac7b705ecd9a1a4b73d73c15eab596d39874e5276a3d9cb88bbb698bafc3f8d08c34cf17
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -19118,8 +19135,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"webpack@npm:^5":
|
"webpack@npm:^5":
|
||||||
version: 5.103.0
|
version: 5.105.0
|
||||||
resolution: "webpack@npm:5.103.0"
|
resolution: "webpack@npm:5.105.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope": "npm:^3.7.7"
|
"@types/eslint-scope": "npm:^3.7.7"
|
||||||
"@types/estree": "npm:^1.0.8"
|
"@types/estree": "npm:^1.0.8"
|
||||||
@@ -19129,10 +19146,10 @@ __metadata:
|
|||||||
"@webassemblyjs/wasm-parser": "npm:^1.14.1"
|
"@webassemblyjs/wasm-parser": "npm:^1.14.1"
|
||||||
acorn: "npm:^8.15.0"
|
acorn: "npm:^8.15.0"
|
||||||
acorn-import-phases: "npm:^1.0.3"
|
acorn-import-phases: "npm:^1.0.3"
|
||||||
browserslist: "npm:^4.26.3"
|
browserslist: "npm:^4.28.1"
|
||||||
chrome-trace-event: "npm:^1.0.2"
|
chrome-trace-event: "npm:^1.0.2"
|
||||||
enhanced-resolve: "npm:^5.17.3"
|
enhanced-resolve: "npm:^5.19.0"
|
||||||
es-module-lexer: "npm:^1.2.1"
|
es-module-lexer: "npm:^2.0.0"
|
||||||
eslint-scope: "npm:5.1.1"
|
eslint-scope: "npm:5.1.1"
|
||||||
events: "npm:^3.2.0"
|
events: "npm:^3.2.0"
|
||||||
glob-to-regexp: "npm:^0.4.1"
|
glob-to-regexp: "npm:^0.4.1"
|
||||||
@@ -19143,15 +19160,15 @@ __metadata:
|
|||||||
neo-async: "npm:^2.6.2"
|
neo-async: "npm:^2.6.2"
|
||||||
schema-utils: "npm:^4.3.3"
|
schema-utils: "npm:^4.3.3"
|
||||||
tapable: "npm:^2.3.0"
|
tapable: "npm:^2.3.0"
|
||||||
terser-webpack-plugin: "npm:^5.3.11"
|
terser-webpack-plugin: "npm:^5.3.16"
|
||||||
watchpack: "npm:^2.4.4"
|
watchpack: "npm:^2.5.1"
|
||||||
webpack-sources: "npm:^3.3.3"
|
webpack-sources: "npm:^3.3.3"
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
webpack-cli:
|
webpack-cli:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
webpack: bin/webpack.js
|
webpack: bin/webpack.js
|
||||||
checksum: 10c0/d0cf86f8cac249874d6f36292e25011413ebb5bae82c48fa78a165a217e63db00b1a1f563f5195070eb17a055c6da4b6ab89fbdd37f781abdda862aa8c0bd623
|
checksum: 10c0/4aea6b976485b5364e122f301c08f48efa84ddb2c0cb5d09f27445d1f2da0b9875cd889e41b58cac3ff05618a9c965be716df52586d151b5f52a7bbed7662174
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user