Skip to content

Commit 6a72e2a

Browse files
hyrskysundflux
andauthored
Improve logging and error handling, fix date formatting (#88)
* Improve logging * Fix incorrect timezone on results message * Format * update siirtovahti > Siirtovahti --------- Co-authored-by: Tarmo Sundström <tarmo.sundstrom@druid.fi>
1 parent 06d5117 commit 6a72e2a

3 files changed

Lines changed: 104 additions & 74 deletions

File tree

conf/kymp.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
"sv": "https://makasiini.hel.ninja/helsinki-logos/helsinki-logo-black-sv-h45.png"
1313
},
1414
"email_subject_confirmation": {
15-
"fi": "Vahvista ajoneuvojen siirtovahdin tilaus",
15+
"fi": "Vahvista ajoneuvojen Siirtovahdin tilaus",
1616
"en": "Confirm your Vehicle Removal Alert Service subscription",
1717
"sv": "Bekräfta beställningen av flyttningsvakten för fordon"
1818
},
1919
"email_subject_expiry": {
20-
"fi": "Ajoneuvojen siirtovahtisi on vanhentumassa",
20+
"fi": "Ajoneuvojen Siirtovahtisi on vanhentumassa",
2121
"en": "Your Vehicle Removal Alert Service subscription is about to expire",
2222
"sv": "Din flyttningsvakt för fordon håller på att gå ut"
2323
},
2424
"email_subject_newhits": {
25-
"fi": "Uusia osumia ajoneuvojen siirtovahdillasi",
25+
"fi": "Uusia osumia ajoneuvojen Siirtovahdillasi",
2626
"en": "The Vehicle Removal Alert Service has found new matches for you",
2727
"sv": "Nya träffar från din flyttningsvakt för fordon"
2828
},
2929
"email_confirmation_title": {
30-
"fi": "Vahvista ajoneuvojen siirtovahdin tilaus Helsingin kaupungilta",
30+
"fi": "Vahvista ajoneuvojen Siirtovahdin tilaus Helsingin kaupungilta",
3131
"en": "Confirm your Vehicle Removal Alert Service subscription with the City of Helsinki",
3232
"sv": "Bekräfta beställning av flyttningsvakten för fordon från Helsingfors stad"
3333
},

src/bin/hav-populate-queue.ts

Lines changed: 94 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ const getNewHitsFromElasticsearch = async (
149149

150150
const matchField = siteConfig.matchField;
151151

152+
console.info(
153+
`Matched ${elasticResponse?.hits?.total?.value ?? 0} hits for ${subscription._id} from ${siteConfig.name}`,
154+
);
155+
152156
// Filter out new hits:
153157
return (elasticResponse?.hits?.hits ?? [])
154158
.filter((hit: { _source?: Record<string, unknown> }) => {
@@ -176,7 +180,7 @@ const getNewHitsFromElasticsearch = async (
176180
* @param siteConfig - The site configuration to process
177181
* @param stats - Statistics object to track processing
178182
* @param isDryRun - Do not write changes
179-
* @return {Promise<void>} A Promise that resolves when processing is complete
183+
* @return A Promise that resolves when processing is complete
180184
*/
181185
const processSiteSubscriptions = async (
182186
server: Server,
@@ -205,8 +209,12 @@ const processSiteSubscriptions = async (
205209

206210
// Process subscriptions sequentially to avoid overwhelming the system
207211
await result.reduce(async (previousPromise, subscription) => {
212+
// @todo move this to a for loop, using
213+
// reduce() here is quite hard to reason.
208214
await previousPromise;
209215

216+
console.info(`Processing subscription ${subscription._id} for site ${siteConfig.id}`);
217+
210218
// Resolve user data from ATV if stored there
211219
let resolvedQuery: string = subscription.query;
212220
let resolvedSearchDescription: string = subscription.search_description ?? '';
@@ -218,8 +226,11 @@ const processSiteSubscriptions = async (
218226
resolvedQuery = atvData.query ?? '';
219227
resolvedSearchDescription = atvData.search_description ?? '';
220228
resolvedElasticQuery = atvData.elastic_query;
229+
230+
console.info(`Subscription details loaded from ATV for ${subscription._id} (site: ${siteConfig.id})`);
221231
} catch (e) {
222232
console.error(`Failed to load user data from ATV for ${subscription._id}`, e);
233+
server.Sentry?.captureException(e);
223234
return Promise.resolve();
224235
}
225236
}
@@ -230,15 +241,16 @@ const processSiteSubscriptions = async (
230241
const subscriptionValidForDays = siteConfig.subscription.maxAge;
231242

232243
// Sync ATV delete_after if needed (handles config changes and legacy subscriptions)
244+
// @todo: why do we need this?
233245
const expectedDeleteAfter = calculateExpectedDeleteAfter(new Date(subscription.created), subscriptionValidForDays);
234246
if (needsDeleteAfterSync(subscription.delete_after, expectedDeleteAfter)) {
235-
if (isDryRun) {
236-
console.log(
237-
`[DRY RUN] Would sync ATV delete_after for ${subscription._id} ` +
238-
`(stored: ${subscription.delete_after?.toISOString().substring(0, 10) ?? 'none'}, ` +
239-
`expected: ${expectedDeleteAfter.toISOString().substring(0, 10)})`,
240-
);
241-
} else {
247+
console.info(
248+
`Sync ATV delete_after for ${subscription._id} ` +
249+
`(stored: ${subscription.delete_after?.toISOString().substring(0, 10) ?? 'none'}, ` +
250+
`expected: ${expectedDeleteAfter.toISOString().substring(0, 10)})`,
251+
);
252+
253+
if (!isDryRun) {
242254
try {
243255
await server.atv.updateDocumentDeleteAfter(
244256
ATV.getAtvId(subscription),
@@ -262,41 +274,50 @@ const processSiteSubscriptions = async (
262274

263275
// If subscription should expire soon, send an expiration email
264276
if (checkShouldSendExpiryNotification(subscription as Partial<SubscriptionCollectionType>, siteConfig)) {
265-
if (isDryRun) {
266-
console.log(`[DRY RUN] Would send expiry email to ${ATV.getAtvId(subscription)} (site: ${siteConfig.id})`);
267-
} else {
277+
console.info(`Sending expiry email to ${ATV.getAtvId(subscription)} (site: ${siteConfig.id})`);
278+
279+
// @fixme: dry run keeps spamming the messages and should
280+
// newer be used in production. Why do we need dry-run feature?
281+
if (!isDryRun) {
268282
await collection.updateOne({ _id: subscription._id }, { $set: { expiry_notification_sent: 1 } });
269283
}
270284

271-
const expiryEmailContent = await expiryEmail(
272-
subscription.lang,
273-
{
274-
search_description: resolvedSearchDescription,
275-
link: siteConfig.urls.base + resolvedQuery,
276-
removal_date: formattedExpiryDate,
277-
remove_link: `${localizedBaseUrl}/hakuvahti/unsubscribe?subscription=${subscription._id}&hash=${subscription.hash}`,
278-
renewal_link: `${localizedBaseUrl}/hakuvahti/renew?subscription=${subscription._id}&hash=${subscription.hash}`,
279-
search_link: resolvedQuery,
280-
},
281-
siteConfig,
282-
);
283-
284285
// Queue expiry email if email is active
285286
if (isEmailActive(subscription as Partial<SubscriptionCollectionType>)) {
286-
const expiryEmailToQueue: QueueInsertDocument = {
287-
type: 'email',
288-
atv_id: ATV.getAtvId(subscription),
289-
content: expiryEmailContent,
290-
};
287+
try {
288+
const expiryEmailContent = await expiryEmail(
289+
subscription.lang,
290+
{
291+
search_description: resolvedSearchDescription,
292+
link: siteConfig.urls.base + resolvedQuery,
293+
removal_date: formattedExpiryDate,
294+
remove_link: `${localizedBaseUrl}/hakuvahti/unsubscribe?subscription=${subscription._id}&hash=${subscription.hash}`,
295+
renewal_link: `${localizedBaseUrl}/hakuvahti/renew?subscription=${subscription._id}&hash=${subscription.hash}`,
296+
search_link: resolvedQuery,
297+
},
298+
siteConfig,
299+
);
291300

292-
if (!isDryRun) {
293-
await queueCollection.insertOne(expiryEmailToQueue);
301+
const expiryEmailToQueue: QueueInsertDocument = {
302+
type: 'email',
303+
atv_id: ATV.getAtvId(subscription),
304+
content: expiryEmailContent,
305+
};
306+
307+
if (!isDryRun) {
308+
await queueCollection.insertOne(expiryEmailToQueue);
309+
}
310+
stats.expiryEmailsQueued++;
311+
} catch (error) {
312+
console.error(`Error queueing expiry email for subscription ${subscription._id}:`, error);
313+
server.Sentry?.captureException(error);
294314
}
295-
stats.expiryEmailsQueued++;
296315
}
297316

298317
// Queue renewal SMS if subscription has SMS and site supports it
299318
if (isSmsActive(subscription as Partial<SubscriptionCollectionType>) && siteConfig.subscription.enableSms) {
319+
console.info(`Sending expiry SMS for ${subscription._id} (site: ${siteConfig.id})`);
320+
300321
try {
301322
const smsContent = await renewalSms(
302323
subscription.lang,
@@ -314,14 +335,14 @@ const processSiteSubscriptions = async (
314335
content: smsContent,
315336
};
316337

317-
if (isDryRun) {
318-
console.log(`[DRY RUN] Would queue renewal SMS for ${subscription._id}`);
338+
if (!isDryRun) {
319339
} else {
320340
await queueCollection.insertOne(smsToQueue);
321341
}
322342
stats.smsQueued++;
323343
} catch (error) {
324344
console.error(`Error queueing renewal SMS for subscription ${subscription._id}:`, error);
345+
server.Sentry?.captureException(error);
325346
}
326347
}
327348
}
@@ -335,6 +356,7 @@ const processSiteSubscriptions = async (
335356

336357
// No new hits
337358
if (newHits.length === 0) {
359+
console.info(`No hits for ${subscription._id} from ${siteConfig.name}`);
338360
return Promise.resolve();
339361
}
340362

@@ -348,41 +370,46 @@ const processSiteSubscriptions = async (
348370
const pad = (n: number) => n.toString().padStart(2, '0');
349371
const formattedCreatedDate = `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()}`;
350372

351-
const emailContent = await newHitsEmail(
352-
subscription.lang,
353-
{
354-
created_date: formattedCreatedDate,
355-
expiry_date: formattedExpiryDate,
356-
search_description: resolvedSearchDescription,
357-
search_link: resolvedQuery,
358-
remove_link: `${localizedBaseUrl}/hakuvahti/unsubscribe?subscription=${subscription._id}&hash=${subscription.hash}`,
359-
hits: hitsForEmail,
360-
},
361-
siteConfig,
362-
);
373+
// Update last_checked regardless of channel
374+
if (!isDryRun) {
375+
const dateUnixtime: number = Math.floor(Date.now() / 1000);
376+
await collection.updateOne({ _id: subscription._id }, { $set: { last_checked: dateUnixtime } });
377+
}
363378

364379
// Queue new hits email if email is active
365380
if (isEmailActive(subscription as Partial<SubscriptionCollectionType>)) {
366-
const email: QueueInsertDocument = {
367-
type: 'email',
368-
atv_id: ATV.getAtvId(subscription),
369-
content: emailContent,
370-
};
371-
372-
if (isDryRun) {
373-
console.log(
374-
`[DRY RUN] Would queue email for ${ATV.getAtvId(subscription)}: ${newHits.length} new result(s) (site: ${siteConfig.id})`,
381+
try {
382+
const emailContent = await newHitsEmail(
383+
subscription.lang,
384+
{
385+
created_date: formattedCreatedDate,
386+
expiry_date: formattedExpiryDate,
387+
search_description: resolvedSearchDescription,
388+
search_link: resolvedQuery,
389+
remove_link: `${localizedBaseUrl}/hakuvahti/unsubscribe?subscription=${subscription._id}&hash=${subscription.hash}`,
390+
hits: hitsForEmail,
391+
},
392+
siteConfig,
375393
);
376-
} else {
377-
await queueCollection.insertOne(email);
378-
}
379-
stats.newResultsEmailsQueued++;
380-
}
381394

382-
// Update last_checked regardless of channel
383-
if (!isDryRun) {
384-
const dateUnixtime: number = Math.floor(Date.now() / 1000);
385-
await collection.updateOne({ _id: subscription._id }, { $set: { last_checked: dateUnixtime } });
395+
const email: QueueInsertDocument = {
396+
type: 'email',
397+
atv_id: ATV.getAtvId(subscription),
398+
content: emailContent,
399+
};
400+
401+
console.info(
402+
`New email for ${ATV.getAtvId(subscription)}: ${newHits.length} new result(s) (site: ${siteConfig.id})`,
403+
);
404+
405+
if (!isDryRun) {
406+
await queueCollection.insertOne(email);
407+
}
408+
stats.newResultsEmailsQueued++;
409+
} catch (error) {
410+
// Log error but don't break email sending
411+
console.error(`Error queueing SMS for subscription ${subscription._id}:`, error);
412+
}
386413
}
387414

388415
// Queue SMS if subscription has SMS confirmed and SMS is enabled for site
@@ -404,9 +431,9 @@ const processSiteSubscriptions = async (
404431
content: smsContent,
405432
};
406433

407-
if (isDryRun) {
408-
console.log(`[DRY RUN] Would queue SMS for ${subscription._id}`);
409-
} else {
434+
console.log(`New SMS for ${subscription._id}: ${newHits.length} new result(s) (site: ${siteConfig.id})`);
435+
436+
if (!isDryRun) {
410437
await queueCollection.insertOne(smsToQueue);
411438
}
412439
stats.smsQueued++;

src/lib/email.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ const TEMPLATE_BASE_PATH = 'dist/templates';
1010
const fieldFormatters: Record<string, (val: unknown, siteConfig: SiteConfigurationType) => string> = {
1111
date: (val) => {
1212
if (typeof val !== 'number') return '';
13-
const d = new Date(val * 1000);
14-
const pad = (n: number) => n.toString().padStart(2, '0');
15-
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()}`;
13+
return new Date(val * 1000).toLocaleDateString('fi-FI', {
14+
timeZone: 'Europe/Helsinki',
15+
day: '2-digit',
16+
month: '2-digit',
17+
year: 'numeric',
18+
});
1619
},
1720
url: (val, siteConfig) => siteConfig.urls.base + String(val ?? ''),
1821
};

0 commit comments

Comments
 (0)