with one click
notifly-default-user-condition-tracing
// Trace whether Notifly campaign default user conditions are actually applied as additional filters, from web-console save path to runtime delivery checks and campaign data verification.
// Trace whether Notifly campaign default user conditions are actually applied as additional filters, from web-console save path to runtime delivery checks and campaign data verification.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | notifly-default-user-condition-tracing |
| description | Trace whether Notifly campaign default user conditions are actually applied as additional filters, from web-console save path to runtime delivery checks and campaign data verification. |
| version | 1.0.0 |
| author | Hermes Agent |
| license | MIT |
| metadata | {"hermes":{"tags":["notifly","campaign","segmentation","default-user-condition","additional-filter","debugging"],"related_skills":["systematic-debugging"]}} |
Use when someone asks whether "๊ธฐ๋ณธ ์ ์ ์กฐ๊ฑด" is working as an "์ถ๊ฐ ํํฐ", or when a campaign appears to have sent to users who should have been excluded.
Establish three things:
segment_info.additional_conditions?services/server/web-console/public/locales/ko/common.json
services/server/web-console/src/utils/campaign/index.ts
enableDefaultUserCondition, defaultChannelConditions.services/server/web-console/src/components/segment/condition/index.tsx
defaultChannelConditions.services/server/web-console/src/utils/campaign/adapter/index.ts
defaultChannelConditions into segment adapter only when enableDefaultUserCondition is true.services/server/web-console/src/utils/campaign/adapter/segment/index.ts
details.additionalConditions.services/server/web-console/src/utils/campaign/defaultValue.ts
view_state for editing; important when interpreting stored data.services/server/web-console/src/schemas/campaign/view/index.ts
packages/segment-helper/src/segment.ts
groupMatched && additionalConditionMatched.packages/segment-helper/src/condition.ts
services/task/segment-publisher/lib/segment/recipients/recipient.ts
matchSegment(...) before publish.services/task/segment-publisher/lib/segment/segment_publisher.ts
matchesSegmentCondition(...) when filtering recipients.services/task/segment-publisher/lib/message/push_notification.ts
additional_conditions.services/lambda/scheduled-batch-delivery/lib/delivery_policy.js
additional_conditions before send.inspectRecipientsWithAdditionalConditions(...) pattern.packages/delivery-policy/src/index.ts
inspectRecipientsWithAdditionalConditions(...).Read the Korean locale strings first.
Look for wording like:
This is important because some incidents are really expectation mismatches, not code bugs.
Verify this path:
enableDefaultUserConditiondefaultChannelConditions stores channel-specific default conditionsdetails.additionalConditionssegment_info.additional_conditionsImportant caveat:
conditionTargetingMode === ALL, the merge is skipped.view_state but not affect runtime filtering for ALL-target campaigns.Use packages/segment-helper/src/segment.ts.
The key model is:
groupMatched && additionalConditionMatchedThis is the cleanest proof that default user conditions, once merged into additional conditions, behave as extra filters.
For scheduled / queued delivery, confirm both layers if relevant:
matchSegment(...)additional_conditions before actual send, especially for delayed deliveryThis matters because even if query extraction is broad, later layers can still enforce the filter.
For a concrete campaign ID, query PostgreSQL campaigns_<projectId> tables.
There are many per-project campaign tables. A reliable approach is:
^campaigns_[0-9a-f]{32}$WHERE id = $1Python + asyncpg works well because psql may not be installed while env vars are present:
POSTGRES_HOSTPOSTGRES_PORTPOSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DBFrom the matching row, inspect both:
segment_infoview_stateInterpretation:
view_state.enableDefaultUserCondition = true means UI toggle was onview_state.defaultChannelConditions shows the configured defaults in editor schemaview_state.segment.details.additionalConditions may be empty by design after the 2026-04-08 changesegment_info.additional_conditions is the runtime truth for actual filteringAlso inspect fields like:
statustiming_typestartscreated_atupdated_atReason: users often refer to a campaign as โdailyโ or โcurrentโ, but the stored campaign may actually be a one-time or terminated campaign. That mismatch is highly diagnostic.
Use Python asyncpg to iterate over campaigns_<projectId> tables and fetch id, name, channel, segment_info, view_state, status.
For push cases, query:
delivery_result_<projectId>campaign_id and channel = 'push-notification'event_nameThis tells you whether the campaign actually sent and roughly how many unique users were involved.
A common trap: reading current values from users_<projectId>.encrypted_user_properties and treating them as the values at send time.
Be careful:
So if current DB values appear inconsistent with send eligibility, phrase the conclusion carefully:
For current projects, user properties typically live in:
users_<projectId>.encrypted_user_propertiesDo not assume user_<projectId> exists; some projects only have the shadowing/encrypted table.
If you need actual plaintext user properties, prefer existing code paths that already do the right thing:
packages/userdb/src/index.ts
executeQueryWithShadowing(...)decryptUserModels(...)services/server/web-console/src/repositories/UserRepository.ts
These paths handle:
encrypted_email, encrypted_phone_number, encrypted_user_propertiesgetCachedPlainDataKey(...)decryptWithAES256CBC(...)superjson.parse(...)Direct decryption requires AWS KMS decrypt permission.
If you try to reimplement decryption manually and hit something like:
AccessDeniedException on kms:Decryptthen the environment can still read PostgreSQL but cannot produce plaintext user properties. In that case:
encrypted_user_properties->>'<attr>'Even without decrypt permission, you can still:
segment_info.additional_conditionsdelivery_result_<projectId>users_<projectId>push, ๋งํธ์คํ์ฌ๋ถ) to find outliersThis is useful for narrowing suspicious recipients, but do not overstate it: encrypted bucket mismatches are not the same as plaintext proof.
ALL targeting mode bypasses default user condition merge
conditionTargetingMode === ALL, defaults do not become runtime additional conditionsExisting campaigns are not auto-updated when setting-level defaults change
Editor schema vs runtime schema differ intentionally
view_state may show defaults in defaultChannelConditionssegment_info.additional_conditions is the operational fieldview_state.segment.details.additionalConditions means defaults were not appliedIf behavior seems surprising, inspect these commits:
2a3fb8356 โ fix(web-console): ๋ชจ์ ๊ณ์ฐ ์ ๊ธฐ๋ณธ ์ ์ ์กฐ๊ฑด ๋ฐ์ (#3418)441a1df71 โ fix(web-console): ๊ธฐ๋ณธ์ ์ ์กฐ๊ฑด์ ์ ์ฅ ์์ ์๋ง mergeํ๋๋ก ์์ (#3404)These explain why the UI/editor may no longer show merged defaults directly while runtime still uses them.
When reporting findings, keep it crisp:
segment_info.additional_conditions for the campaign.enableDefaultUserCondition=true์ด๊ณ runtime segment_info.additional_conditions์ ์กฐ๊ฑด์ด ์ ์ฅ๋์ด ์์ต๋๋ค."