backend-manager 5.9.23 → 5.9.25
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/docs/ghostii.md +27 -7
- package/package.json +1 -1
- package/src/manager/events/cron/daily/blog-auto-publisher.js +75 -15
- package/src/manager/libraries/content/source-resolver.js +30 -1
- package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
- package/src/test/fixtures/firebase-project/database-debug.log +4 -8
- package/src/test/fixtures/firebase-project/firestore-debug.log +1 -91
- package/src/test/fixtures/firebase-project/pubsub-debug.log +2 -4
- package/test/routes/marketing/campaign.js +8 -1
package/docs/ghostii.md
CHANGED
|
@@ -49,18 +49,19 @@ Each `blog.content[]` entry has a `sources` array. The cron picks one at random
|
|
|
49
49
|
|
|
50
50
|
1. Fetch and parse the RSS/Atom/JSON feed via `feed-parser.parseFeed()`
|
|
51
51
|
2. Query `content-sources` in Firestore for already-processed items
|
|
52
|
-
3. Filter to unprocessed items
|
|
53
|
-
4.
|
|
54
|
-
5.
|
|
55
|
-
6.
|
|
56
|
-
7.
|
|
52
|
+
3. Filter to unprocessed items; also filter items whose title is too similar to recently published articles (cross-feed dedup via `isTitleTooSimilar`)
|
|
53
|
+
4. Pick the first remaining item
|
|
54
|
+
5. Extract full article content from the item URL via `feed-parser.extractArticleContent()`
|
|
55
|
+
6. Pass extracted text as `sourceContent` to the provider API (separate from the `description` prompt)
|
|
56
|
+
7. After publish, write a tracking doc to `content-sources/{hash}`
|
|
57
|
+
8. On failure (feed unreachable, unparseable, exhausted): return null so `harvest()` tries the next source. Only falls back to `$brand` if `$brand` is explicitly in the entry's `sources` array.
|
|
57
58
|
|
|
58
59
|
### Parent source flow
|
|
59
60
|
|
|
60
61
|
1. Fetch available sources from parent server via `GET {parentUrl}/newsletter-sources` (no `claimFor` -- read-only)
|
|
61
62
|
2. Query `content-sources` in Firestore for already-used sources with `origin: '$parent'`
|
|
62
63
|
3. Filter out already-used sources, pick the first available
|
|
63
|
-
4. On failure (no parent URL, no sources, all exhausted):
|
|
64
|
+
4. On failure (no parent URL, no sources, all exhausted): return null so `harvest()` tries the next source. Only falls back to `$brand` if `$brand` is explicitly in the entry's `sources` array.
|
|
64
65
|
|
|
65
66
|
### Content source tracking
|
|
66
67
|
|
|
@@ -74,7 +75,8 @@ content-sources/{sha256(origin + '::' + url).slice(0,20)}: {
|
|
|
74
75
|
origin: '$feed:https://...', // or '$parent', '$brand'
|
|
75
76
|
feedUrl: 'https://...', // for feed sources
|
|
76
77
|
itemId: 'guid-or-url',
|
|
77
|
-
itemTitle: '...',
|
|
78
|
+
itemTitle: '...', // source article title
|
|
79
|
+
postTitle: '...', // generated article title (for topic dedup)
|
|
78
80
|
usedBy: 'blog', // or 'newsletter'
|
|
79
81
|
brandId: '...',
|
|
80
82
|
postUrl: '...',
|
|
@@ -86,6 +88,24 @@ content-sources/{sha256(origin + '::' + url).slice(0,20)}: {
|
|
|
86
88
|
}
|
|
87
89
|
```
|
|
88
90
|
|
|
91
|
+
### Topic deduplication
|
|
92
|
+
|
|
93
|
+
The blog auto-publisher prevents near-duplicate articles at three levels:
|
|
94
|
+
|
|
95
|
+
1. **Per-feed dedup**: `processFeedSource()` queries `content-sources` by `feedUrl` to skip items already used from that specific feed.
|
|
96
|
+
|
|
97
|
+
2. **Cross-feed dedup** (structural): Before picking a feed item, `processFeedSource()` compares each candidate's title against ALL recent titles (from any source) using `isTitleTooSimilar()` — a word-overlap check with basic stemming. This catches the same news event reported by different publications (e.g. Guardian and NYT both covering Apple's price hike). Items with >= 50% significant-word overlap are skipped.
|
|
98
|
+
|
|
99
|
+
3. **Topic dedup** (prompt-based): Before generating each article, `harvest()` appends all recent titles (Firestore history + same-run) to the Ghostii prompt as a strict avoidance list. The prompt forbids writing about the same topic, theme, or keyword combination — not just the same story.
|
|
100
|
+
|
|
101
|
+
Within a single cron run, both generated post titles AND source item titles are tracked in `runTitles` and fed into levels 2 and 3 for subsequent articles.
|
|
102
|
+
|
|
103
|
+
All source types (`$feed`, `$parent`, `$brand`, URL, text) are tracked in `content-sources` after publishing.
|
|
104
|
+
|
|
105
|
+
### Source fallback behavior
|
|
106
|
+
|
|
107
|
+
When a source fails or is exhausted, `resolveSource()` only falls back to `$brand` if `$brand` is explicitly listed in the entry's `sources` array. Otherwise it returns null, and `harvest()` tries the next source in a shuffled copy of the pool. If all sources are exhausted, that article slot is skipped.
|
|
108
|
+
|
|
89
109
|
## Configuration
|
|
90
110
|
|
|
91
111
|
```js
|
package/package.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*
|
|
16
16
|
* All feed/parent sources are tracked in Firestore (`content-sources`) so the
|
|
17
17
|
* same article is never processed twice. When a feed is unreachable or
|
|
18
|
-
* exhausted, the
|
|
18
|
+
* exhausted, harvest() tries the next source in the pool. Only falls
|
|
19
|
+
* back to $brand if '$brand' is explicitly listed in the entry's sources.
|
|
19
20
|
*
|
|
20
21
|
* Source resolution, tracking, and prompt constants live in the shared
|
|
21
22
|
* source-resolver library (src/manager/libraries/content/source-resolver.js).
|
|
@@ -32,6 +33,7 @@ const {
|
|
|
32
33
|
trackContentSource,
|
|
33
34
|
contentSourceHash,
|
|
34
35
|
getProcessedItemIds,
|
|
36
|
+
getRecentTitles,
|
|
35
37
|
getURLContent,
|
|
36
38
|
isURL,
|
|
37
39
|
} = require('../../../libraries/content/source-resolver.js');
|
|
@@ -118,19 +120,49 @@ function fetchRemoteBrand(brandUrl) {
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
async function harvest(assistant, entry, admin, provider, Manager) {
|
|
121
|
-
const date = moment().format('MMMM YYYY');
|
|
122
|
-
|
|
123
123
|
assistant.log(`harvest(): Starting ${entry.brand.brand.id}...`);
|
|
124
124
|
|
|
125
|
+
const recentTitles = admin
|
|
126
|
+
? await getRecentTitles(admin, entry.brand.brand.id).catch((e) => {
|
|
127
|
+
assistant.error('harvest(): Error fetching recent titles (continuing)', e);
|
|
128
|
+
return [];
|
|
129
|
+
})
|
|
130
|
+
: [];
|
|
131
|
+
const runTitles = [];
|
|
132
|
+
|
|
133
|
+
if (recentTitles.length) {
|
|
134
|
+
assistant.log(`harvest(): ${recentTitles.length} recent titles loaded for topic dedup`);
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
for (let index = 0; index < entry.quantity; index++) {
|
|
126
|
-
const
|
|
138
|
+
const allKnownTitles = [...recentTitles, ...runTitles];
|
|
127
139
|
|
|
128
|
-
|
|
140
|
+
let resolved = null;
|
|
141
|
+
const shuffled = randomize([...entry.sources]);
|
|
142
|
+
for (const source of shuffled) {
|
|
143
|
+
assistant.log(`harvest(): Processing ${index + 1}/${entry.quantity}`, source);
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
resolved = await resolveSource(assistant, source, entry, admin, Manager).catch((e) => {
|
|
146
|
+
assistant.error('harvest(): Error resolving source', e);
|
|
147
|
+
return null;
|
|
148
|
+
});
|
|
149
|
+
if (resolved) { break; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!resolved) {
|
|
153
|
+
assistant.log('harvest(): All sources exhausted for this article, skipping');
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (allKnownTitles.length) {
|
|
158
|
+
resolved.description += '\n\nTOPIC DEDUPLICATION (STRICT):\n'
|
|
159
|
+
+ 'These articles were RECENTLY published on our blog. You MUST NOT write about the same topic, theme, or subject area as ANY of them.\n'
|
|
160
|
+
+ 'Do NOT cover the same story from a different angle, do NOT write about the same theme with different examples, '
|
|
161
|
+
+ 'and do NOT reuse the same primary keyword/category combination. '
|
|
162
|
+
+ 'If a recent article covers "lifestyle tech + habits", do NOT write about "lifestyle tech + policy" — that is the SAME theme. '
|
|
163
|
+
+ 'Pick an entirely different domain.\n'
|
|
164
|
+
+ 'Recent articles:\n'
|
|
165
|
+
+ allKnownTitles.slice(0, 25).map((t) => `- ${t}`).join('\n');
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
assistant.log('harvest(): Resolved source', resolved);
|
|
@@ -154,6 +186,9 @@ async function harvest(assistant, entry, admin, provider, Manager) {
|
|
|
154
186
|
|
|
155
187
|
assistant.log('harvest(): Article', article);
|
|
156
188
|
|
|
189
|
+
const post = provider.blocksToPost?.(article.json);
|
|
190
|
+
const generatedTitle = post?.title || article.title || '';
|
|
191
|
+
|
|
157
192
|
const publishArgs = {
|
|
158
193
|
brand: entry.brand,
|
|
159
194
|
article,
|
|
@@ -172,12 +207,28 @@ async function harvest(assistant, entry, admin, provider, Manager) {
|
|
|
172
207
|
|
|
173
208
|
assistant.log('harvest(): Uploaded post', uploadedPost);
|
|
174
209
|
|
|
175
|
-
if (
|
|
210
|
+
if (generatedTitle) {
|
|
211
|
+
runTitles.push(generatedTitle);
|
|
212
|
+
}
|
|
213
|
+
if (resolved.trackingData?.itemTitle) {
|
|
214
|
+
runTitles.push(resolved.trackingData.itemTitle);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!resolved.trackingData) {
|
|
218
|
+
resolved.trackingData = {
|
|
219
|
+
url: uploadedPost.slug || `brand-${postId}`,
|
|
220
|
+
origin: '$brand',
|
|
221
|
+
usedBy: 'blog',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (admin) {
|
|
176
226
|
await trackContentSource(admin, {
|
|
177
227
|
...resolved.trackingData,
|
|
178
228
|
brandId: entry.brand.brand.id,
|
|
179
229
|
postUrl: uploadedPost.url,
|
|
180
230
|
postSlug: uploadedPost.slug,
|
|
231
|
+
postTitle: generatedTitle || null,
|
|
181
232
|
}).catch((e) => {
|
|
182
233
|
assistant.error('harvest(): Error tracking content source (non-fatal)', e);
|
|
183
234
|
});
|
|
@@ -205,8 +256,11 @@ async function resolveSource(assistant, source, entry, admin, Manager) {
|
|
|
205
256
|
const feedResult = await processFeedSource(assistant, feedUrl, entry.brand.brand.id, admin).catch((e) => e);
|
|
206
257
|
|
|
207
258
|
if (feedResult instanceof Error || !feedResult) {
|
|
208
|
-
assistant.log('resolveSource(): Feed failed or exhausted
|
|
209
|
-
|
|
259
|
+
assistant.log('resolveSource(): Feed failed or exhausted');
|
|
260
|
+
if (entry.sources.includes('$brand')) {
|
|
261
|
+
return resolveSource(assistant, '$brand', entry, admin, Manager);
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
210
264
|
}
|
|
211
265
|
|
|
212
266
|
const description = powertools.template(PROMPT_SOURCE, {
|
|
@@ -235,8 +289,11 @@ async function resolveSource(assistant, source, entry, admin, Manager) {
|
|
|
235
289
|
const parentResults = await processParentSource(assistant, entry, admin, Manager).catch((e) => e);
|
|
236
290
|
|
|
237
291
|
if (parentResults instanceof Error || !parentResults?.length) {
|
|
238
|
-
assistant.log('resolveSource(): Parent source failed or exhausted
|
|
239
|
-
|
|
292
|
+
assistant.log('resolveSource(): Parent source failed or exhausted');
|
|
293
|
+
if (entry.sources.includes('$brand')) {
|
|
294
|
+
return resolveSource(assistant, '$brand', entry, admin, Manager);
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
240
297
|
}
|
|
241
298
|
|
|
242
299
|
const parentResult = parentResults[0];
|
|
@@ -275,7 +332,10 @@ async function resolveSource(assistant, source, entry, admin, Manager) {
|
|
|
275
332
|
|
|
276
333
|
if (suggestion instanceof Error) {
|
|
277
334
|
assistant.error(`resolveSource(): Error fetching URL ${source}`, suggestion);
|
|
278
|
-
|
|
335
|
+
if (entry.sources.includes('$brand')) {
|
|
336
|
+
return resolveSource(assistant, '$brand', entry, admin, Manager);
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
279
339
|
}
|
|
280
340
|
|
|
281
341
|
const description = powertools.template(PROMPT, {
|
|
@@ -269,7 +269,7 @@ async function resolveNewsletterSources({ sources, categories, admin, Manager, a
|
|
|
269
269
|
|
|
270
270
|
// ── Firestore tracking ──────────────────────────────────────────────
|
|
271
271
|
|
|
272
|
-
async function trackContentSource(admin, { url, origin, feedUrl, itemId, itemTitle, usedBy, brandId, postUrl, postSlug }) {
|
|
272
|
+
async function trackContentSource(admin, { url, origin, feedUrl, itemId, itemTitle, usedBy, brandId, postUrl, postSlug, postTitle }) {
|
|
273
273
|
const docId = contentSourceHash(origin || '', url || '');
|
|
274
274
|
const nowISO = new Date().toISOString();
|
|
275
275
|
const nowUNIX = Math.round(Date.now() / 1000);
|
|
@@ -280,6 +280,7 @@ async function trackContentSource(admin, { url, origin, feedUrl, itemId, itemTit
|
|
|
280
280
|
feedUrl: feedUrl || null,
|
|
281
281
|
itemId: itemId || null,
|
|
282
282
|
itemTitle: itemTitle || null,
|
|
283
|
+
postTitle: postTitle || null,
|
|
283
284
|
usedBy: usedBy || null,
|
|
284
285
|
brandId: brandId || null,
|
|
285
286
|
postUrl: postUrl || null,
|
|
@@ -374,6 +375,33 @@ function tryParse(json) {
|
|
|
374
375
|
}
|
|
375
376
|
}
|
|
376
377
|
|
|
378
|
+
// ── Recent title loading (topic dedup) ───────────────────────────────
|
|
379
|
+
|
|
380
|
+
async function getRecentTitles(admin, brandId, days) {
|
|
381
|
+
if (!admin) { return []; }
|
|
382
|
+
|
|
383
|
+
days = days || 14;
|
|
384
|
+
const cutoff = Math.round(Date.now() / 1000) - (days * 86400);
|
|
385
|
+
|
|
386
|
+
const snapshot = await admin.firestore()
|
|
387
|
+
.collection(CONTENT_SOURCES_COLLECTION)
|
|
388
|
+
.where('brandId', '==', brandId)
|
|
389
|
+
.select('postTitle', 'itemTitle', 'metadata')
|
|
390
|
+
.get();
|
|
391
|
+
|
|
392
|
+
const titles = new Set();
|
|
393
|
+
snapshot.docs.forEach((doc) => {
|
|
394
|
+
const data = doc.data();
|
|
395
|
+
const created = data.metadata?.created?.timestampUNIX || 0;
|
|
396
|
+
if (created < cutoff) { return; }
|
|
397
|
+
|
|
398
|
+
if (data.postTitle) { titles.add(data.postTitle); }
|
|
399
|
+
if (data.itemTitle) { titles.add(data.itemTitle); }
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return [...titles];
|
|
403
|
+
}
|
|
404
|
+
|
|
377
405
|
// ── Exports ─────────────────────────────────────────────────────────
|
|
378
406
|
|
|
379
407
|
module.exports = {
|
|
@@ -391,6 +419,7 @@ module.exports = {
|
|
|
391
419
|
trackContentSource,
|
|
392
420
|
contentSourceHash,
|
|
393
421
|
getProcessedItemIds,
|
|
422
|
+
getRecentTitles,
|
|
394
423
|
getURLContent,
|
|
395
424
|
isURL,
|
|
396
425
|
extractBodyContent,
|
|
@@ -2,11 +2,7 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
|
2
2
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by akka.util.Unsafe (file:/Users/ian/.cache/firebase/emulators/firebase-database-emulator-v4.11.2.jar)
|
|
3
3
|
WARNING: Please consider reporting this to the maintainers of class akka.util.Unsafe
|
|
4
4
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
18:53:55.860 [Thread-0] INFO com.firebase.server.forge.App$ - Attempting graceful shutdown.
|
|
10
|
-
18:53:55.865 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.Terminator$Terminator - 1 actors left to terminate: demo-backend-manager-default-rtdb
|
|
11
|
-
18:53:55.869 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - stopped namespace actor for demo-backend-manager-default-rtdb
|
|
12
|
-
18:53:55.872 [Thread-0] INFO com.firebase.server.forge.App$ - Graceful shutdown complete.
|
|
5
|
+
02:27:39.303 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
|
|
6
|
+
02:27:39.394 [main] INFO com.firebase.server.forge.App$ - Listening at 127.0.0.1:9000
|
|
7
|
+
02:27:48.479 [Thread-0] INFO com.firebase.server.forge.App$ - Attempting graceful shutdown.
|
|
8
|
+
02:27:48.481 [Thread-0] INFO com.firebase.server.forge.App$ - Graceful shutdown complete.
|
|
@@ -2,7 +2,7 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
|
2
2
|
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/Users/ian/.cache/firebase/emulators/cloud-firestore-emulator-v1.21.0.jar)
|
|
3
3
|
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
|
|
4
4
|
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
|
|
5
|
-
Jun
|
|
5
|
+
Jun 30, 2026 2:27:38 AM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start
|
|
6
6
|
INFO: Started WebSocket server on ws://127.0.0.1:9150
|
|
7
7
|
|
|
8
8
|
API endpoint: http://127.0.0.1:8080
|
|
@@ -20,95 +20,5 @@ If you are running a Firestore in Datastore Mode project, run:
|
|
|
20
20
|
Note: Support for Datastore Mode is in preview. If you encounter any bugs please file at https://github.com/firebase/firebase-tools/issues.
|
|
21
21
|
Dev App Server is now running.
|
|
22
22
|
|
|
23
|
-
Jun 29, 2026 6:53:43 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
24
|
-
INFO: Detected non-HTTP/2 connection.
|
|
25
|
-
Jun 29, 2026 6:53:43 PM io.grpc.netty.TcpMetrics loadEpollInfo
|
|
26
|
-
INFO: Epoll available during static init of TcpMetrics:false
|
|
27
|
-
Jun 29, 2026 6:53:43 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
28
|
-
INFO: Detected HTTP/2 connection.
|
|
29
|
-
Jun 29, 2026 6:53:45 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
30
|
-
INFO: Detected HTTP/2 connection.
|
|
31
|
-
Jun 29, 2026 6:53:45 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
32
|
-
INFO: Detected HTTP/2 connection.
|
|
33
|
-
Jun 29, 2026 6:53:53 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
34
|
-
INFO: Detected HTTP/2 connection.
|
|
35
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
36
|
-
INFO: Detected HTTP/2 connection.
|
|
37
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
38
|
-
INFO: Detected HTTP/2 connection.
|
|
39
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
40
|
-
INFO: Detected HTTP/2 connection.
|
|
41
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
42
|
-
INFO: Detected HTTP/2 connection.
|
|
43
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
44
|
-
INFO: Detected HTTP/2 connection.
|
|
45
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
46
|
-
INFO: Detected HTTP/2 connection.
|
|
47
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
48
|
-
INFO: Detected HTTP/2 connection.
|
|
49
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
50
|
-
INFO: Detected HTTP/2 connection.
|
|
51
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
52
|
-
INFO: Detected HTTP/2 connection.
|
|
53
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
54
|
-
INFO: Detected HTTP/2 connection.
|
|
55
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
56
|
-
INFO: Detected HTTP/2 connection.
|
|
57
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
58
|
-
INFO: Detected HTTP/2 connection.
|
|
59
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
60
|
-
INFO: Detected HTTP/2 connection.
|
|
61
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
62
|
-
INFO: Detected HTTP/2 connection.
|
|
63
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
64
|
-
INFO: Detected HTTP/2 connection.
|
|
65
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
66
|
-
INFO: Detected HTTP/2 connection.
|
|
67
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
68
|
-
INFO: Detected HTTP/2 connection.
|
|
69
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
70
|
-
INFO: Detected HTTP/2 connection.
|
|
71
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
72
|
-
INFO: Detected HTTP/2 connection.
|
|
73
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
74
|
-
INFO: Detected HTTP/2 connection.
|
|
75
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
76
|
-
INFO: Detected HTTP/2 connection.
|
|
77
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
78
|
-
INFO: Detected HTTP/2 connection.
|
|
79
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
80
|
-
INFO: Detected HTTP/2 connection.
|
|
81
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
82
|
-
INFO: Detected HTTP/2 connection.
|
|
83
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
84
|
-
INFO: Detected HTTP/2 connection.
|
|
85
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
86
|
-
INFO: Detected HTTP/2 connection.
|
|
87
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
88
|
-
INFO: Detected HTTP/2 connection.
|
|
89
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
90
|
-
INFO: Detected HTTP/2 connection.
|
|
91
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
92
|
-
INFO: Detected HTTP/2 connection.
|
|
93
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
94
|
-
INFO: Detected HTTP/2 connection.
|
|
95
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
96
|
-
INFO: Detected HTTP/2 connection.
|
|
97
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
98
|
-
INFO: Detected HTTP/2 connection.
|
|
99
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
100
|
-
INFO: Detected HTTP/2 connection.
|
|
101
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
102
|
-
INFO: Detected HTTP/2 connection.
|
|
103
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
104
|
-
INFO: Detected HTTP/2 connection.
|
|
105
|
-
Jun 29, 2026 6:53:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
106
|
-
INFO: Detected HTTP/2 connection.
|
|
107
|
-
Jun 29, 2026 6:53:55 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
108
|
-
INFO: Detected HTTP/2 connection.
|
|
109
|
-
Jun 29, 2026 6:53:55 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
110
|
-
INFO: Detected HTTP/2 connection.
|
|
111
|
-
Jun 29, 2026 6:53:55 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
112
|
-
INFO: Detected HTTP/2 connection.
|
|
113
23
|
*** shutting down gRPC server since JVM is shutting down
|
|
114
24
|
*** server shut down
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
This is the Google Pub/Sub fake.
|
|
2
2
|
Implementation may be incomplete or differ from the real system.
|
|
3
|
-
Jun
|
|
3
|
+
Jun 30, 2026 2:27:42 AM com.google.cloud.pubsub.testing.v1.Main main
|
|
4
4
|
INFO: IAM integration is disabled. IAM policy methods and ACL checks are not supported
|
|
5
5
|
SLF4J(W): No SLF4J providers were found.
|
|
6
6
|
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
|
|
@@ -9,9 +9,7 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
|
9
9
|
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/Users/ian/.cache/firebase/emulators/pubsub-emulator-0.8.33/pubsub-emulator/lib/cloud-pubsub-emulator-0.8.33-all.jar)
|
|
10
10
|
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
|
|
11
11
|
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
|
|
12
|
-
Jun
|
|
12
|
+
Jun 30, 2026 2:27:42 AM com.google.cloud.pubsub.testing.v1.Main main
|
|
13
13
|
INFO: Server started, listening on 8085
|
|
14
|
-
Jun 29, 2026 6:53:43 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
15
|
-
INFO: Detected HTTP/2 connection.
|
|
16
14
|
*** shutting down gRPC server since JVM is shutting down
|
|
17
15
|
*** server shut down
|
|
@@ -22,7 +22,14 @@ module.exports = {
|
|
|
22
22
|
const response = await http.post('backend-manager/marketing/campaign', {
|
|
23
23
|
name: 'BEM Test Campaign',
|
|
24
24
|
subject: 'Test Marketing Email',
|
|
25
|
-
|
|
25
|
+
template: 'card',
|
|
26
|
+
data: {
|
|
27
|
+
content: {
|
|
28
|
+
title: 'Test Marketing Email',
|
|
29
|
+
message: 'This is a **test marketing email** sent from the BEM test suite.\n\nIf you received this, the SendGrid Single Send pipeline is working correctly.',
|
|
30
|
+
button: { text: 'Visit Dashboard →', url: '{brand.url}' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
26
33
|
test: true,
|
|
27
34
|
sendAt: 'now',
|
|
28
35
|
});
|