Files
awesome-reviewers/_reviewers/posthog-verify-error-handling-paths.json
2025-08-19 12:19:58 +00:00

70 lines
16 KiB
JSON

[
{
"discussion_id": "2275746221",
"pr_number": 36271,
"pr_file": "plugin-server/src/cdp/services/messaging/recipient-preferences.service.ts",
"created_at": "2025-08-14T07:36:24+00:00",
"commented_code": "+import { HogFlowAction } from '../../../schema/hogflow'\n+import { CyclotronJobInvocationHogFlow } from '../../types'\n+import { RecipientsManagerService } from '../managers/recipients-manager.service'\n+\n+export class RecipientPreferencesService {\n+ constructor(private recipientsManager: RecipientsManagerService) {}\n+\n+ public async shouldSkipAction(invocation: CyclotronJobInvocationHogFlow, action: HogFlowAction): Promise<boolean> {\n+ return (\n+ this.isSubjectToRecipientPreferences(action) && (await this.isRecipientOptedOutOfAction(invocation, action))\n+ )\n+ }\n+\n+ private isSubjectToRecipientPreferences(\n+ action: HogFlowAction\n+ ): action is Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }> {\n+ return ['function_email', 'function_sms'].includes(action.type)\n+ }\n+\n+ private async isRecipientOptedOutOfAction(\n+ invocation: CyclotronJobInvocationHogFlow,\n+ action: Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }>\n+ ): Promise<boolean> {\n+ // Get the identifier to be used from the action config for sms, this is an input called to_number,\n+ // for email it is inside an input called email, specifically email.to.\n+ let identifier\n+\n+ if (action.type === 'function_sms') {\n+ identifier = action.config.inputs?.to_number\n+ } else if (action.type === 'function_email') {\n+ identifier = action.config.inputs?.email?.value?.to\n+ }\n+\n+ if (!identifier) {\n+ throw new Error(`No identifier found for message action ${action.id}`)\n+ }\n+\n+ try {\n+ const recipient = await this.recipientsManager.get({\n+ teamId: invocation.teamId,\n+ identifier: identifier,\n+ })\n+\n+ if (recipient) {\n+ // Grab the recipient preferences for the action category\n+ const categoryId = action.config.message_category_id || '$all'\n+\n+ const messageCategoryPreference = this.recipientsManager.getPreference(recipient, categoryId)\n+ const allMarketingPreferences = this.recipientsManager.getAllMarketingMessagingPreference(recipient)\n+\n+ /**\n+ * NB: A recipient may have opted out of all marketing messaging but NOT a specific category,\n+ * so we always check both.\n+ *\n+ * This would commonly happen if the recipient opted out before the category was created.\n+ */\n+ if (messageCategoryPreference === 'OPTED_OUT' || allMarketingPreferences === 'OPTED_OUT') {\n+ return true\n+ }\n+ }\n+\n+ return false",
"repo_full_name": "PostHog/posthog",
"discussion_comments": [
{
"comment_id": "2275746221",
"repo_full_name": "PostHog/posthog",
"pr_number": 36271,
"pr_file": "plugin-server/src/cdp/services/messaging/recipient-preferences.service.ts",
"discussion_id": "2275746221",
"commented_code": "@@ -0,0 +1,69 @@\n+import { HogFlowAction } from '../../../schema/hogflow'\n+import { CyclotronJobInvocationHogFlow } from '../../types'\n+import { RecipientsManagerService } from '../managers/recipients-manager.service'\n+\n+export class RecipientPreferencesService {\n+ constructor(private recipientsManager: RecipientsManagerService) {}\n+\n+ public async shouldSkipAction(invocation: CyclotronJobInvocationHogFlow, action: HogFlowAction): Promise<boolean> {\n+ return (\n+ this.isSubjectToRecipientPreferences(action) && (await this.isRecipientOptedOutOfAction(invocation, action))\n+ )\n+ }\n+\n+ private isSubjectToRecipientPreferences(\n+ action: HogFlowAction\n+ ): action is Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }> {\n+ return ['function_email', 'function_sms'].includes(action.type)\n+ }\n+\n+ private async isRecipientOptedOutOfAction(\n+ invocation: CyclotronJobInvocationHogFlow,\n+ action: Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }>\n+ ): Promise<boolean> {\n+ // Get the identifier to be used from the action config for sms, this is an input called to_number,\n+ // for email it is inside an input called email, specifically email.to.\n+ let identifier\n+\n+ if (action.type === 'function_sms') {\n+ identifier = action.config.inputs?.to_number\n+ } else if (action.type === 'function_email') {\n+ identifier = action.config.inputs?.email?.value?.to\n+ }\n+\n+ if (!identifier) {\n+ throw new Error(`No identifier found for message action ${action.id}`)\n+ }\n+\n+ try {\n+ const recipient = await this.recipientsManager.get({\n+ teamId: invocation.teamId,\n+ identifier: identifier,\n+ })\n+\n+ if (recipient) {\n+ // Grab the recipient preferences for the action category\n+ const categoryId = action.config.message_category_id || '$all'\n+\n+ const messageCategoryPreference = this.recipientsManager.getPreference(recipient, categoryId)\n+ const allMarketingPreferences = this.recipientsManager.getAllMarketingMessagingPreference(recipient)\n+\n+ /**\n+ * NB: A recipient may have opted out of all marketing messaging but NOT a specific category,\n+ * so we always check both.\n+ *\n+ * This would commonly happen if the recipient opted out before the category was created.\n+ */\n+ if (messageCategoryPreference === 'OPTED_OUT' || allMarketingPreferences === 'OPTED_OUT') {\n+ return true\n+ }\n+ }\n+\n+ return false",
"comment_created_at": "2025-08-14T07:36:24+00:00",
"comment_author": "meikelmosby",
"comment_body": "so this means if we do not find an recipient we return `false`?",
"pr_file_module": null
},
{
"comment_id": "2283351466",
"repo_full_name": "PostHog/posthog",
"pr_number": 36271,
"pr_file": "plugin-server/src/cdp/services/messaging/recipient-preferences.service.ts",
"discussion_id": "2275746221",
"commented_code": "@@ -0,0 +1,69 @@\n+import { HogFlowAction } from '../../../schema/hogflow'\n+import { CyclotronJobInvocationHogFlow } from '../../types'\n+import { RecipientsManagerService } from '../managers/recipients-manager.service'\n+\n+export class RecipientPreferencesService {\n+ constructor(private recipientsManager: RecipientsManagerService) {}\n+\n+ public async shouldSkipAction(invocation: CyclotronJobInvocationHogFlow, action: HogFlowAction): Promise<boolean> {\n+ return (\n+ this.isSubjectToRecipientPreferences(action) && (await this.isRecipientOptedOutOfAction(invocation, action))\n+ )\n+ }\n+\n+ private isSubjectToRecipientPreferences(\n+ action: HogFlowAction\n+ ): action is Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }> {\n+ return ['function_email', 'function_sms'].includes(action.type)\n+ }\n+\n+ private async isRecipientOptedOutOfAction(\n+ invocation: CyclotronJobInvocationHogFlow,\n+ action: Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }>\n+ ): Promise<boolean> {\n+ // Get the identifier to be used from the action config for sms, this is an input called to_number,\n+ // for email it is inside an input called email, specifically email.to.\n+ let identifier\n+\n+ if (action.type === 'function_sms') {\n+ identifier = action.config.inputs?.to_number\n+ } else if (action.type === 'function_email') {\n+ identifier = action.config.inputs?.email?.value?.to\n+ }\n+\n+ if (!identifier) {\n+ throw new Error(`No identifier found for message action ${action.id}`)\n+ }\n+\n+ try {\n+ const recipient = await this.recipientsManager.get({\n+ teamId: invocation.teamId,\n+ identifier: identifier,\n+ })\n+\n+ if (recipient) {\n+ // Grab the recipient preferences for the action category\n+ const categoryId = action.config.message_category_id || '$all'\n+\n+ const messageCategoryPreference = this.recipientsManager.getPreference(recipient, categoryId)\n+ const allMarketingPreferences = this.recipientsManager.getAllMarketingMessagingPreference(recipient)\n+\n+ /**\n+ * NB: A recipient may have opted out of all marketing messaging but NOT a specific category,\n+ * so we always check both.\n+ *\n+ * This would commonly happen if the recipient opted out before the category was created.\n+ */\n+ if (messageCategoryPreference === 'OPTED_OUT' || allMarketingPreferences === 'OPTED_OUT') {\n+ return true\n+ }\n+ }\n+\n+ return false",
"comment_created_at": "2025-08-18T20:09:42+00:00",
"comment_author": "havenbarnes",
"comment_body": "Hmm yes nice catch - I thought this was the correct behavior but actually it should be `true`. I'll leave a comment saying so, but if someone's never given their preference to PostHog, we can assume they've opted in to messaging via the PostHog user's app / TOS",
"pr_file_module": null
},
{
"comment_id": "2283638210",
"repo_full_name": "PostHog/posthog",
"pr_number": 36271,
"pr_file": "plugin-server/src/cdp/services/messaging/recipient-preferences.service.ts",
"discussion_id": "2275746221",
"commented_code": "@@ -0,0 +1,69 @@\n+import { HogFlowAction } from '../../../schema/hogflow'\n+import { CyclotronJobInvocationHogFlow } from '../../types'\n+import { RecipientsManagerService } from '../managers/recipients-manager.service'\n+\n+export class RecipientPreferencesService {\n+ constructor(private recipientsManager: RecipientsManagerService) {}\n+\n+ public async shouldSkipAction(invocation: CyclotronJobInvocationHogFlow, action: HogFlowAction): Promise<boolean> {\n+ return (\n+ this.isSubjectToRecipientPreferences(action) && (await this.isRecipientOptedOutOfAction(invocation, action))\n+ )\n+ }\n+\n+ private isSubjectToRecipientPreferences(\n+ action: HogFlowAction\n+ ): action is Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }> {\n+ return ['function_email', 'function_sms'].includes(action.type)\n+ }\n+\n+ private async isRecipientOptedOutOfAction(\n+ invocation: CyclotronJobInvocationHogFlow,\n+ action: Extract<HogFlowAction, { type: 'function_email' | 'function_sms' }>\n+ ): Promise<boolean> {\n+ // Get the identifier to be used from the action config for sms, this is an input called to_number,\n+ // for email it is inside an input called email, specifically email.to.\n+ let identifier\n+\n+ if (action.type === 'function_sms') {\n+ identifier = action.config.inputs?.to_number\n+ } else if (action.type === 'function_email') {\n+ identifier = action.config.inputs?.email?.value?.to\n+ }\n+\n+ if (!identifier) {\n+ throw new Error(`No identifier found for message action ${action.id}`)\n+ }\n+\n+ try {\n+ const recipient = await this.recipientsManager.get({\n+ teamId: invocation.teamId,\n+ identifier: identifier,\n+ })\n+\n+ if (recipient) {\n+ // Grab the recipient preferences for the action category\n+ const categoryId = action.config.message_category_id || '$all'\n+\n+ const messageCategoryPreference = this.recipientsManager.getPreference(recipient, categoryId)\n+ const allMarketingPreferences = this.recipientsManager.getAllMarketingMessagingPreference(recipient)\n+\n+ /**\n+ * NB: A recipient may have opted out of all marketing messaging but NOT a specific category,\n+ * so we always check both.\n+ *\n+ * This would commonly happen if the recipient opted out before the category was created.\n+ */\n+ if (messageCategoryPreference === 'OPTED_OUT' || allMarketingPreferences === 'OPTED_OUT') {\n+ return true\n+ }\n+ }\n+\n+ return false",
"comment_created_at": "2025-08-18T22:34:51+00:00",
"comment_author": "havenbarnes",
"comment_body": "Oops no, the `false` is correct. Forgot this is inside `isRecipientOptedOutOfAction`. I'll refactor this and add a comment still",
"pr_file_module": null
}
]
},
{
"discussion_id": "2260699270",
"pr_number": 35926,
"pr_file": "plugin-server/src/worker/ingestion/persons/repositories/postgres-person-repository.ts",
"created_at": "2025-08-07T15:32:54+00:00",
"commented_code": "}\n }\n \n+ if (this.isPropertiesSizeConstraintViolation(error)) {\n+ // For createPerson, we just log and reject since there's no existing person to update\n+ personPropertiesSizeViolationCounter.inc({\n+ violation_type: 'create_person_size_violation',\n+ })\n+\n+ logger.warn('Rejecting person properties create/update, exceeds size limit', {\n+ team_id: teamId,\n+ person_id: undefined,\n+ violation_type: 'create_person_size_violation',\n+ })\n+\n+ throw new PersonPropertiesSizeViolationError(\n+ `Person properties create would exceed size limit`,\n+ teamId,\n+ undefined\n+ )",
"repo_full_name": "PostHog/posthog",
"discussion_comments": [
{
"comment_id": "2260699270",
"repo_full_name": "PostHog/posthog",
"pr_number": 35926,
"pr_file": "plugin-server/src/worker/ingestion/persons/repositories/postgres-person-repository.ts",
"discussion_id": "2260699270",
"commented_code": "@@ -213,6 +423,25 @@ export class PostgresPersonRepository\n }\n }\n \n+ if (this.isPropertiesSizeConstraintViolation(error)) {\n+ // For createPerson, we just log and reject since there's no existing person to update\n+ personPropertiesSizeViolationCounter.inc({\n+ violation_type: 'create_person_size_violation',\n+ })\n+\n+ logger.warn('Rejecting person properties create/update, exceeds size limit', {\n+ team_id: teamId,\n+ person_id: undefined,\n+ violation_type: 'create_person_size_violation',\n+ })\n+\n+ throw new PersonPropertiesSizeViolationError(\n+ `Person properties create would exceed size limit`,\n+ teamId,\n+ undefined\n+ )",
"comment_created_at": "2025-08-07T15:32:54+00:00",
"comment_author": "pl",
"comment_body": "question: Are we failing gracefully in this case? I could not find the code that handles the size violation exceptions when creating a person, but I might be looking wrong. I think it would be good to test it at the `person-*-service` level. ",
"pr_file_module": null
}
]
}
]