payload-plugin-newsletter 0.16.5 → 0.16.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## [0.16.7] - 2025-07-29
2
+
3
+ ### Added
4
+ - **Comprehensive Diagnostic Logging** - Added extensive logging to diagnose broadcast sync issues
5
+ - Logs the HTML content conversion process
6
+ - Shows exactly what data is being sent to the Broadcast API (with preview)
7
+ - Displays request URL, method, and body structure
8
+ - Shows API response status and headers
9
+ - Captures and logs all error types (Error objects, strings, JSON responses)
10
+ - Logs raw errors to identify unexpected error formats
11
+
12
+ ### Improved
13
+ - **Better Error Visibility** - Enhanced error handling to capture more details
14
+ - Raw error logging to see the actual error structure
15
+ - Multiple error format handlers (Error, string, object, unknown)
16
+ - Response body parsing for API errors
17
+ - Document context logging when errors occur
18
+ - API request/response details in provider logs
19
+
20
+ ### Technical
21
+ - Added pre-API call logging in afterChange hook
22
+ - Added comprehensive error logging in BroadcastApiProvider
23
+ - Logs help identify if issues are with content, API format, or authentication
24
+
25
+ ## [0.16.6] - 2025-07-29
26
+
27
+ ### Fixed
28
+ - **Critical: Update Sync Now Works** - Fixed the afterChange hook that was blocking update operations
29
+ - Removed the `operation !== 'create'` check that prevented the afterChange hook from running on updates
30
+ - Moved update sync logic from beforeChange to afterChange for proper architectural pattern
31
+ - Updates are now synced AFTER they're saved to Payload, ensuring consistency
32
+ - Provider sync failures no longer block Payload updates
33
+
34
+ ### Improved
35
+ - **Better Hook Architecture** - Sync operations now happen in the correct lifecycle stage
36
+ - beforeChange was architecturally wrong - if provider sync failed, data would be inconsistent
37
+ - afterChange ensures Payload data is saved first, then syncs to provider
38
+ - More resilient to network failures and API errors
39
+
40
+ ### Technical
41
+ - Consolidated create and update logic in a single afterChange hook
42
+ - Added comprehensive content change detection before syncing
43
+ - Enhanced logging for update sync operations
44
+ - Removed redundant beforeChange hook logic
45
+
1
46
  ## [0.16.5] - 2025-07-29
2
47
 
3
48
  ### Breaking Changes
@@ -252,29 +252,46 @@ var init_broadcast2 = __esm({
252
252
  async create(data) {
253
253
  try {
254
254
  this.validateRequiredFields(data, ["name", "subject", "content"]);
255
+ const requestBody = {
256
+ broadcast: {
257
+ name: data.name,
258
+ subject: data.subject,
259
+ preheader: data.preheader,
260
+ body: data.content,
261
+ html_body: true,
262
+ track_opens: data.trackOpens ?? true,
263
+ track_clicks: data.trackClicks ?? true,
264
+ reply_to: data.replyTo,
265
+ segment_ids: data.audienceIds
266
+ }
267
+ };
268
+ console.log("[BroadcastApiProvider] Creating broadcast:", {
269
+ url: `${this.apiUrl}/api/v1/broadcasts`,
270
+ method: "POST",
271
+ hasToken: !!this.token,
272
+ tokenLength: this.token?.length,
273
+ body: JSON.stringify(requestBody, null, 2)
274
+ });
255
275
  const response = await fetch(`${this.apiUrl}/api/v1/broadcasts`, {
256
276
  method: "POST",
257
277
  headers: {
258
278
  "Authorization": `Bearer ${this.token}`,
259
279
  "Content-Type": "application/json"
260
280
  },
261
- body: JSON.stringify({
262
- broadcast: {
263
- name: data.name,
264
- subject: data.subject,
265
- preheader: data.preheader,
266
- body: data.content,
267
- html_body: true,
268
- track_opens: data.trackOpens ?? true,
269
- track_clicks: data.trackClicks ?? true,
270
- reply_to: data.replyTo,
271
- segment_ids: data.audienceIds
272
- }
273
- })
281
+ body: JSON.stringify(requestBody)
274
282
  });
283
+ console.log("[BroadcastApiProvider] Response status:", response.status);
284
+ console.log("[BroadcastApiProvider] Response headers:", Object.fromEntries(response.headers.entries()));
275
285
  if (!response.ok) {
276
- const error = await response.text();
277
- throw new Error(`Broadcast API error: ${response.status} - ${error}`);
286
+ const errorText = await response.text();
287
+ console.error("[BroadcastApiProvider] Error response body:", errorText);
288
+ let errorDetails;
289
+ try {
290
+ errorDetails = JSON.parse(errorText);
291
+ console.error("[BroadcastApiProvider] Parsed error:", errorDetails);
292
+ } catch {
293
+ }
294
+ throw new Error(`Broadcast API error: ${response.status} - ${errorText}`);
278
295
  }
279
296
  const result = await response.json();
280
297
  return this.get(result.id.toString());
@@ -1435,58 +1452,162 @@ var createBroadcastsCollection = (pluginConfig) => {
1435
1452
  }
1436
1453
  ],
1437
1454
  hooks: {
1438
- // Sync with provider on create
1455
+ // Sync with provider on create and update
1439
1456
  afterChange: [
1440
- async ({ doc, operation, req }) => {
1441
- if (!hasProviders || operation !== "create") return doc;
1442
- try {
1443
- const providerConfig = await getBroadcastConfig(req, pluginConfig);
1444
- if (!providerConfig || !providerConfig.token) {
1445
- req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1446
- return doc;
1447
- }
1448
- const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1449
- const provider = new BroadcastApiProvider2(providerConfig);
1450
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
1451
- const providerBroadcast = await provider.create({
1452
- name: doc.subject,
1453
- // Use subject as name since we removed the name field
1454
- subject: doc.subject,
1455
- preheader: doc.contentSection?.preheader,
1456
- content: htmlContent,
1457
- trackOpens: doc.settings?.trackOpens,
1458
- trackClicks: doc.settings?.trackClicks,
1459
- replyTo: doc.settings?.replyTo || providerConfig.replyTo,
1460
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
1461
- });
1462
- await req.payload.update({
1463
- collection: "broadcasts",
1464
- id: doc.id,
1465
- data: {
1457
+ async ({ doc, operation, req, previousDoc }) => {
1458
+ if (!hasProviders) return doc;
1459
+ if (operation === "create") {
1460
+ try {
1461
+ const providerConfig = await getBroadcastConfig(req, pluginConfig);
1462
+ if (!providerConfig || !providerConfig.token) {
1463
+ req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1464
+ return doc;
1465
+ }
1466
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1467
+ const provider = new BroadcastApiProvider2(providerConfig);
1468
+ req.payload.logger.info("Converting content to HTML...");
1469
+ const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
1470
+ const createData = {
1471
+ name: doc.subject,
1472
+ // Use subject as name since we removed the name field
1473
+ subject: doc.subject,
1474
+ preheader: doc.contentSection?.preheader,
1475
+ content: htmlContent,
1476
+ trackOpens: doc.settings?.trackOpens,
1477
+ trackClicks: doc.settings?.trackClicks,
1478
+ replyTo: doc.settings?.replyTo || providerConfig.replyTo,
1479
+ audienceIds: doc.audienceIds?.map((a) => a.audienceId)
1480
+ };
1481
+ req.payload.logger.info("Creating broadcast with data:", {
1482
+ name: createData.name,
1483
+ subject: createData.subject,
1484
+ preheader: createData.preheader || "NONE",
1485
+ contentLength: htmlContent ? htmlContent.length : 0,
1486
+ contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
1487
+ trackOpens: createData.trackOpens,
1488
+ trackClicks: createData.trackClicks,
1489
+ replyTo: createData.replyTo,
1490
+ audienceIds: createData.audienceIds || [],
1491
+ apiUrl: providerConfig.apiUrl,
1492
+ hasToken: !!providerConfig.token
1493
+ });
1494
+ const providerBroadcast = await provider.create(createData);
1495
+ await req.payload.update({
1496
+ collection: "broadcasts",
1497
+ id: doc.id,
1498
+ data: {
1499
+ providerId: providerBroadcast.id,
1500
+ providerData: providerBroadcast.providerData
1501
+ },
1502
+ req
1503
+ });
1504
+ return {
1505
+ ...doc,
1466
1506
  providerId: providerBroadcast.id,
1467
1507
  providerData: providerBroadcast.providerData
1468
- },
1469
- req
1470
- });
1471
- return {
1472
- ...doc,
1473
- providerId: providerBroadcast.id,
1474
- providerData: providerBroadcast.providerData
1475
- };
1476
- } catch (error) {
1477
- if (error instanceof Error) {
1478
- req.payload.logger.error("Failed to create broadcast in provider:", {
1479
- message: error.message,
1480
- stack: error.stack,
1481
- name: error.name,
1482
- // If it's a BroadcastProviderError, it might have additional details
1483
- ...error.details
1508
+ };
1509
+ } catch (error) {
1510
+ req.payload.logger.error("Raw error from broadcast provider:");
1511
+ req.payload.logger.error(error);
1512
+ if (error instanceof Error) {
1513
+ req.payload.logger.error("Error is instance of Error:", {
1514
+ message: error.message,
1515
+ stack: error.stack,
1516
+ name: error.name,
1517
+ // If it's a BroadcastProviderError, it might have additional details
1518
+ ...error.details,
1519
+ // Check if it's a fetch response error
1520
+ ...error.response,
1521
+ ...error.data,
1522
+ ...error.status,
1523
+ ...error.statusText
1524
+ });
1525
+ } else if (typeof error === "string") {
1526
+ req.payload.logger.error("Error is a string:", error);
1527
+ } else if (error && typeof error === "object") {
1528
+ req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
1529
+ } else {
1530
+ req.payload.logger.error("Unknown error type:", typeof error);
1531
+ }
1532
+ req.payload.logger.error("Failed broadcast document:", {
1533
+ id: doc.id,
1534
+ subject: doc.subject,
1535
+ hasContent: !!doc.contentSection?.content,
1536
+ contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
1484
1537
  });
1485
- } else {
1486
- req.payload.logger.error("Failed to create broadcast in provider:", error);
1538
+ return doc;
1487
1539
  }
1488
- return doc;
1489
1540
  }
1541
+ if (operation === "update" && doc.providerId) {
1542
+ req.payload.logger.info("Broadcast afterChange update hook triggered", {
1543
+ operation,
1544
+ hasProviderId: !!doc.providerId,
1545
+ sendStatus: doc.sendStatus,
1546
+ publishStatus: doc._status
1547
+ });
1548
+ try {
1549
+ const providerConfig = await getBroadcastConfig(req, pluginConfig);
1550
+ if (!providerConfig || !providerConfig.token) {
1551
+ req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1552
+ return doc;
1553
+ }
1554
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1555
+ const provider = new BroadcastApiProvider2(providerConfig);
1556
+ const capabilities = provider.getCapabilities();
1557
+ const sendStatus = doc.sendStatus || "draft" /* DRAFT */;
1558
+ if (!capabilities.editableStatuses.includes(sendStatus)) {
1559
+ req.payload.logger.info(`Skipping sync for broadcast in status: ${sendStatus}`);
1560
+ return doc;
1561
+ }
1562
+ const contentChanged = doc.subject !== previousDoc?.subject || doc.contentSection?.preheader !== previousDoc?.contentSection?.preheader || JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content) || doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens || doc.settings?.trackClicks !== previousDoc?.settings?.trackClicks || doc.settings?.replyTo !== previousDoc?.settings?.replyTo || JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds);
1563
+ if (contentChanged) {
1564
+ const updates = {};
1565
+ if (doc.subject !== previousDoc?.subject) {
1566
+ updates.name = doc.subject;
1567
+ updates.subject = doc.subject;
1568
+ }
1569
+ if (doc.contentSection?.preheader !== previousDoc?.contentSection?.preheader) {
1570
+ updates.preheader = doc.contentSection?.preheader;
1571
+ }
1572
+ if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
1573
+ updates.content = await convertToEmailSafeHtml(doc.contentSection?.content);
1574
+ }
1575
+ if (doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens) {
1576
+ updates.trackOpens = doc.settings.trackOpens;
1577
+ }
1578
+ if (doc.settings?.trackClicks !== previousDoc?.settings?.trackClicks) {
1579
+ updates.trackClicks = doc.settings.trackClicks;
1580
+ }
1581
+ if (doc.settings?.replyTo !== previousDoc?.settings?.replyTo) {
1582
+ updates.replyTo = doc.settings.replyTo || providerConfig.replyTo;
1583
+ }
1584
+ if (JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds)) {
1585
+ updates.audienceIds = doc.audienceIds?.map((a) => a.audienceId);
1586
+ }
1587
+ req.payload.logger.info("Syncing broadcast updates to provider", {
1588
+ providerId: doc.providerId,
1589
+ updates
1590
+ });
1591
+ await provider.update(doc.providerId, updates);
1592
+ req.payload.logger.info(`Broadcast ${doc.id} synced to provider successfully`);
1593
+ } else {
1594
+ req.payload.logger.info("No content changes to sync to provider");
1595
+ }
1596
+ } catch (error) {
1597
+ if (error instanceof Error) {
1598
+ req.payload.logger.error("Failed to sync broadcast update to provider:", {
1599
+ message: error.message,
1600
+ stack: error.stack,
1601
+ name: error.name,
1602
+ // If it's a BroadcastProviderError, it might have additional details
1603
+ ...error.details
1604
+ });
1605
+ } else {
1606
+ req.payload.logger.error("Failed to sync broadcast update to provider:", error);
1607
+ }
1608
+ }
1609
+ }
1610
+ return doc;
1490
1611
  },
1491
1612
  // Hook to send when published
1492
1613
  async ({ doc, operation, previousDoc, req }) => {
@@ -1544,79 +1665,8 @@ var createBroadcastsCollection = (pluginConfig) => {
1544
1665
  return doc;
1545
1666
  }
1546
1667
  ],
1547
- // Sync updates with provider
1548
- beforeChange: [
1549
- async ({ data, originalDoc, operation, req }) => {
1550
- if (!hasProviders || !originalDoc?.providerId || operation !== "update") return data;
1551
- req.payload.logger.info("Broadcast beforeChange update hook triggered", {
1552
- operation,
1553
- hasProviderId: !!originalDoc?.providerId,
1554
- originalSendStatus: originalDoc?.sendStatus,
1555
- originalPublishStatus: originalDoc?._status,
1556
- newSendStatus: data?.sendStatus,
1557
- newPublishStatus: data?._status
1558
- });
1559
- try {
1560
- const providerConfig = await getBroadcastConfig(req, pluginConfig);
1561
- if (!providerConfig || !providerConfig.token) {
1562
- req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1563
- return data;
1564
- }
1565
- const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1566
- const provider = new BroadcastApiProvider2(providerConfig);
1567
- const capabilities = provider.getCapabilities();
1568
- const sendStatus = originalDoc.sendStatus || "draft" /* DRAFT */;
1569
- if (!capabilities.editableStatuses.includes(sendStatus)) {
1570
- req.payload.logger.info(`Skipping sync for broadcast in status: ${sendStatus}`);
1571
- return data;
1572
- }
1573
- const updates = {};
1574
- if (data.subject !== originalDoc.subject) {
1575
- updates.name = data.subject;
1576
- updates.subject = data.subject;
1577
- }
1578
- if (data.contentSection?.preheader !== originalDoc.contentSection?.preheader) updates.preheader = data.contentSection?.preheader;
1579
- if (data.contentSection?.content !== originalDoc.contentSection?.content) {
1580
- updates.content = await convertToEmailSafeHtml(data.contentSection?.content);
1581
- }
1582
- if (data.settings?.trackOpens !== originalDoc.settings?.trackOpens) {
1583
- updates.trackOpens = data.settings.trackOpens;
1584
- }
1585
- if (data.settings?.trackClicks !== originalDoc.settings?.trackClicks) {
1586
- updates.trackClicks = data.settings.trackClicks;
1587
- }
1588
- if (data.settings?.replyTo !== originalDoc.settings?.replyTo) {
1589
- updates.replyTo = data.settings.replyTo || providerConfig.replyTo;
1590
- }
1591
- if (JSON.stringify(data.audienceIds) !== JSON.stringify(originalDoc.audienceIds)) {
1592
- updates.audienceIds = data.audienceIds?.map((a) => a.audienceId);
1593
- }
1594
- if (Object.keys(updates).length > 0) {
1595
- req.payload.logger.info("Syncing broadcast updates to provider", {
1596
- providerId: originalDoc.providerId,
1597
- updates
1598
- });
1599
- await provider.update(originalDoc.providerId, updates);
1600
- req.payload.logger.info("Successfully synced broadcast updates to provider");
1601
- } else {
1602
- req.payload.logger.info("No broadcast updates to sync to provider");
1603
- }
1604
- } catch (error) {
1605
- if (error instanceof Error) {
1606
- req.payload.logger.error("Failed to update broadcast in provider:", {
1607
- message: error.message,
1608
- stack: error.stack,
1609
- name: error.name,
1610
- // If it's a BroadcastProviderError, it might have additional details
1611
- ...error.details
1612
- });
1613
- } else {
1614
- req.payload.logger.error("Failed to update broadcast in provider:", error);
1615
- }
1616
- }
1617
- return data;
1618
- }
1619
- ],
1668
+ // beforeChange hooks can be added here if needed
1669
+ beforeChange: [],
1620
1670
  // Handle deletion
1621
1671
  afterDelete: [
1622
1672
  async ({ doc, req }) => {