feat(da): move DA event page creation into EMC browser at runtime#200
Draft
qiyundai wants to merge 6 commits into
Draft
feat(da): move DA event page creation into EMC browser at runtime#200qiyundai wants to merge 6 commits into
qiyundai wants to merge 6 commits into
Conversation
Ports the DA page creation pipeline from the hoolihan-da-webhook AIO app
into the EMC browser frontend so page creation failures are surfaced to
the author immediately instead of silently disappearing into Splunk logs.
## What's in this commit
### Config (daConfig.ts)
- DA_CONFIG: org='adobecom', branch='main', defaultSite='da-events'
- TARGET_CMS_SITE_MAP: maps series.targetCms.code → DA site name
- DEFAULT_SP_LOCALES: IETF locale → DA folder-prefix map (mirrors
hoolihan-da-webhook appConfig.js defaultLocales)
- HELIX_OPERATIONS enum and getDaSiteForSeries() helper
- Extended ALLOWED_HOSTS in constants.ts: admin.da.live + admin.hlx.page
- Added pageCreatedBy?: string to EventApiResponse in domain.ts
- Registered pageCreatedBy in EVENT_DATA_FILTER (submittable:true) so it
survives prepareEslEventPutPayload and reaches ESL for pipeline suppression
### Network layer (services/da/)
- daFetch.ts: sibling to safeFetch for DA/Helix calls; allows text/html
responses and FormData bodies; validates host via isValidUrl; honors
dryRun + ?nonInvasiveTest=true
- daClient.ts: readFromDA (GET→text), writeToDA (POST multipart FormData),
listDaPath (GET list), deleteFromDA (DELETE); all using Accept:text/html
on reads (not Content-Type)
- helixClient.ts: helixPreview/Publish/Unpublish/DeletePreview/PurgeCache;
resolveHelixOperation({published, liveUpdate}) mapping; bulkHelixOperation
sequencing (publish→preview+publish+purge; unpublish→unpublish+purge;
preview→preview+purge concurrently)
### Page engine (utils/daPage/)
- placeholders.ts: replacePlaceholders ([[kebab.path]] and [[scope:path:index]]
syntaxes); parseRegularPath; resolveArrayPlaceholders; updateFragmentPaths;
camelToKebab; getMetadata; isPrimitive. Ported from utils.js — String.replaceAll
replaced with String.replace(/.../g) for ES2020 compat.
- paths.ts: handleExtension, getRelativeEventPagePath, constructFragmentsFolderPath,
getLocalizedTemplatePath; local joinPath() replaces node:path.join()
- localization.ts: mergeData, mergeLocalization, removeLocalizationObjects,
getDisplayDateTime, formatDate, isSameDate, extractCustomAttributes (reads
primaryProductName + promotionalItems from customAttributes[] with fallback
to publishingProfile.metadata), fillMissingFields. Pure JS, no DOM/Node deps.
- dom.ts: parseHtmlDocument/serializeDocument via native DOMParser (linkedom
replaced); extractDynamicFragmentMetadata, updatePictureTags, getHubConfigs,
createMetaTag, appendEventMetadata, addPageMarker, performDomOperations.
Serialization: <!DOCTYPE html>${documentElement.outerHTML}
### Orchestrator (daPageService.ts)
- isDocumentAuthoringEvent(): checks series.targetCms.code.startsWith('da-')
or series.targetCms.provider === 'documentauthoring'
- createEventPages(): per-locale loop using event.ietfLocales or
event.localizations keys + defaultLocale; per-locale: fetch template via
_templateCache, populateEventObject (mergeLocalization + extractCustomAttributes
+ fillMissingFields), createHydratedEventFragments (dynamic-fragment-metadata
block → fetch fragment templates → hydrate + write to DA), hub pages
(session-hub + speaker-hub rows), picture tag updates, DOM operations
(strip dynamic-fragment-metadata, append full event metadata dump, add
modified-by:EMC marker), write final page, run bulkHelixOperation
- DA ownership: modified-by:EMC marker prevents hoolihan-da-webhook from
overwriting EMC-created pages during dual-run transition period
- Module-level _templateCache Map: caches fetched templates within a session
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d UX fixes
## Save flow integration (useEventFormSave.ts)
- Added step 7b after cachedApi.getEventFull() hydration: if the series is
DA-targeted, calls useDaPageCreation.createPages() with the fully-hydrated
event (image URLs, speakers, sessions, series.templateId all present)
- Publish failure (daResult.success=false + publish=true): throws an Error so
the save is marked failed. A published event with no live page is the worst
outcome — this surfaces immediately to the author via the existing saveError
toast rather than silently failing in Splunk.
- Draft failure (daResult.success=false + publish=false): logs to console,
sets daWarning on SaveResult — the draft WAS saved to ESP so calling
setSaveError would show a red error toast on a successful save, which is
misleading. daWarning lets the caller choose the presentation separately.
- Success: fire-and-forget best-effort PUT to persist pageCreatedBy:'emc' on
the event record so the kinesis-processor can suppress the old webhook path
once it is updated to check the flag. Non-fatal if this PUT fails.
- Extended SaveResult interface: added daWarning?: string field.
## React hook (useDaPageCreation.ts)
- Thin wrapper over daPageService.createEventPages()
- Guards: skips non-DA events (isDocumentAuthoringEvent check) and events
with no detailPagePath; returns {success:true} noop in both cases
- Deduplicates concurrent calls via inFlightRef
- Gets auth token via apiService.getAuthTokenForExternalUse() with null guard
- Exposes: createPages(opts), status, localeResults, error, reset
## EventForm.tsx UX
- handleSave and executeSaveWithUrl: check result.daWarning after saveDraft()
returns; show toast.info() with 10s duration so it is visually distinct
from the success toast (amber info vs green success). Draft save still
shows success toast — the warning is additive, not a replacement.
## Bug fixes
- daClient.ts readFromDA: removed incorrect Content-Type:text/html from GET
request headers (Content-Type describes request body, not response format);
replaced with Accept:text/html
- daPageService.ts listPagePaths: replaced raw fetch() with listDaPath() from
daClient so the ALLOWED_HOSTS allowlist and dry-run/nonInvasiveTest guards
are honoured consistently. listDaPath throws on non-OK; wrapped in try/catch
to preserve the existing empty-array fallback behaviour.
## Barrel exports
- services/da/index.ts: re-exports all public symbols from daFetch, daClient,
helixClient, daPageService (including DaPageCreationInput/Result types)
- utils/daPage/index.ts: re-exports all public symbols from placeholders,
paths, localization, dom
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three test suites covering the pure-JS (no DOM, no Node) utility layer of the DA page creation engine: ### placeholders.test.ts (37 tests) - isPrimitive: null, string, number, boolean (true); object, array, function (false) - camelToKebab: standard conversion, already-kebab input, consecutive-uppercase boundary behaviour (only splits at [a-z][A-Z] transitions, not consecutive caps) - getMetadata: top-level key, deeply-nested key, nested array, missing key - parseRegularPath: colon-delimited array indexing, dot-delimited object walk, out-of-bounds index fallback, missing key fallback - replacePlaceholders: simple [[key]], nested [[parent.child]], special photoURL→photo.imageUrl mapping, missing-key passthrough, colon-syntax array placeholder, multiple replacements in one string - resolveArrayPlaceholders: found with exists:true, no placeholder → null, empty array → exists:false - updateFragmentPaths: link/relativeValue/absoluteValue types, unmatched placeholder passthrough, indexed array placeholders ### paths.test.ts (13 tests) - handleExtension: .docx strip, .xlsx→.json, index.docx→folder-index, normalization + lowercase, leading/trailing hyphen strip - constructFragmentsFolderPath: simple, deep, no-leading-slash paths - getLocalizedTemplatePath: unchanged when no locale, locale inserted after first segment (fr, uk), throws on non-slash-prefixed path - getRelativeEventPagePath: default locale, missing detailPagePath → null, non-default locale includes folder prefix ### localization.test.ts (17 tests) - mergeData: scalar, override precedence, nested objects, array-by-index - removeLocalizationObjects: removes localizations/localizationOverrides from top-level and from array items and nested objects - mergeLocalization: merges locale overrides end-to-end; leaves data unchanged when no locale match - isSameDate: same day different times → true, different days → false - extractCustomAttributes: from customAttributes[], sorted by displayOrder for promotionalItems, publishingProfile.metadata fallback, does not mutate original - fillMissingFields: adds missing photos/series/localizations, preserves existing All tests pass (67/67). Jest 29 + ts-jest in node environment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te, DRY
Three blocking fixes and three warnings from post-commit review:
## Blocking
**Series data injection (useEventFormSave.ts — step 7a)**
`getEventFull` fetches event + speakers + sponsors + venue + images but never
fetches the series record. `isDocumentAuthoringEvent` reads `series.targetCms.code`;
`daPageService` reads `series.templateId` and `series.contentRoot`. Without this
both values are always `undefined` and the entire DA block was dead code.
Fix: after `getEventFull`, if `hydratedEvent.series` is absent, call
`cachedApi.getSeriesFull(seriesId)` and inject the result. The series is already
cached from form initialisation, so this is a near-zero-cost cache hit.
**UTC gmtOffset=0 (localization.ts — getDisplayDateTime)**
`if (...&& gmtOffset)` is falsy for `0` (UTC). Any event hosted in UTC would
silently return `undefined` from `getDisplayDateTime`, leaving the
`[[display-date-time]]` placeholder unresolved in the DA page template.
Fix: `gmtOffset !== undefined && gmtOffset !== null`.
**helixDelete raw fetch (helixClient.ts)**
`helixDelete` bypassed `daFetch` — and therefore the `isValidUrl` ALLOWED_HOSTS
guard and dry-run gate — with a comment claiming "daFetch throws on non-ok, so
handle separately." HTTP 204 has `response.ok === true`, so daFetch never would
have thrown. The special-case was based on a false premise.
Fix: replace with a plain `daFetch` call.
## Warnings
**speakerHubFragmentMap omitted from event page (daPageService.ts)**
Main event page `createOrUpdateDaPage` received only `{ ...sessionHubFragmentMap }`.
If the event page template references speaker-hub fragments via
`[[fragment.speaker-*]]` placeholders, those cross-references would silently
fail. Fix: `{ ...sessionHubFragmentMap, ...speakerHubFragmentMap }`.
**Dead HELIX_OPERATIONS entries (daConfig.ts)**
`CACHEPURGE: 'Purge'` and `DELETE_PREVIEW: 'deletePreview'` were never used —
`helixPurgeCache` and `helixDeletePreview` hardcode their path segments.
Passing them to `bulkHelixOperation` would silently fall through to PREVIEW
(the else branch). Removed both constants.
**joinPath duplicated across 4 files (paths.ts / daClient.ts / dom.ts / daPageService.ts)**
Exported `joinPath` from `utils/daPage/paths.ts` (updated to accept
`(string | undefined | null)[]` with `filter(Boolean)` so callers with optional
parts continue to work). Removed local copies from `daClient.ts`, `dom.ts`, and
`daPageService.ts`. Removed the now-redundant re-export of `constructFragmentsFolderPath`
from `dom.ts` (it's already in the `daPage/index.ts` barrel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Actionable guide for the three other teams whose repos must be updated before the old Kinesis→ESL→Hoolihan DA pipeline can be retired. Covers exact file paths, function names, and before/after code diffs for: - events-service-layer: accept + persist + forward pageCreatedBy field - events-service-platform/ServiceProcessor.js: skip DA webhook tagging when pageCreatedBy==='emc' (behind SUPPRESS_DA_WEBHOOK_FOR_EMC env gate) - events-platform-hh-webhooks/index.js: defensive early-return on flag; documents that checkFileOwner already guards via modified-by:EMC marker Includes rollout order (ESL first — hard prerequisite), per-environment verification checklist, and retirement steps once cutover is stable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the cleanup pass once backward compatibility is no longer needed — i.e. after all DA event pages in prod carry modified-by:EMC and the old webhook has been retired long enough to be confident nothing will write DA_WEBHOOK pages again. EMC removals: fire-and-forget pageCreatedBy follow-up PUT, pageCreatedBy from EVENT_DATA_FILTER and EventApiResponse, modified-by:EMC addPageMarker call, EMC_MARKER + DA_MARKER constants, series injection step 7a. Includes a note on the simplification opportunity: moving the isDocumentAuthoringEvent check to before the save using form context data, which eliminates the getSeriesFull round-trip entirely. Cross-repo removals: pageCreatedBy + targetCmsProvider from ESL schema and kinesis payload; DA-provider forwarding block + SUPPRESS gate + constants from ServiceProcessor.js; hoolihan-da-webhook action directory + secrets if not already deleted in the prior retirement phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hoolihan-da-webhook) as the sole mechanism for DA event page creation. When that pipeline fails, EMC has no visibility — failures only appear in Splunk logs that few people can read.pageCreatedBy: 'emc'flag in the ESP payload so both paths don't race during the dual-run transition.What's in this PR
New: DA service layer (
web-src/src/services/da/)daFetch.tstext/htmlresponses andFormDatabodies; validates hosts viaALLOWED_HOSTS; honoursdryRun+?nonInvasiveTest=truedaClient.tsreadFromDA,writeToDA,listDaPath,deleteFromDAagainstadmin.da.livehelixClient.tshelixPreview/Publish/Unpublish/PurgeCache;resolveHelixOperation;bulkHelixOperationsequencingdaPageService.tsNew: Page engine utilities (
web-src/src/utils/daPage/)placeholders.ts[[kebab.path]]and[[scope:path:index]]template tokenizer;resolveArrayPlaceholders;updateFragmentPathspaths.tsgetRelativeEventPagePath,constructFragmentsFolderPath,getLocalizedTemplatePath; sharedjoinPathlocalization.tsmergeLocalization,extractCustomAttributes(readsprimaryProductName+promotionalItems),getDisplayDateTime,fillMissingFieldsdom.tsDOMParser/XMLSerializerport of linkedom DOM operations: fragment metadata extraction, picture tag updates, metadata block append,modified-byownership markerNew: Config & hook
config/daConfig.ts—DA_CONFIG,DEFAULT_SP_LOCALES,HELIX_OPERATIONS,getDaSiteForSeries()hooks/useDaPageCreation.ts— React hook wrappingdaPageService.createEventPages(); tracksstatus,localeResults,errorModified: Save flow (
hooks/useEventFormSave.ts)getEventFulldoesn't fetch the series record but the DA orchestrator needsseries.targetCmsandseries.templateId)createDaPages()aftergetEventFullhydration so image URLs, speakers, and series data are all present; publish failure throws (blocks save); draft failure setsdaWarningonSaveResultinstead ofsetSaveError(the draft DID save to ESP successfully)pageCreatedBy: 'emc'after successful page creation to suppress the kinesis pipeline once the other repos are updatedModified: EventForm UX (
pages/EventForm/EventForm.tsx)toast.info()whenresult.daWarningis set (DA page creation failed on draft save, ESP save succeeded)Modified: Shared types & filters
ALLOWED_HOSTSinconstants.ts— addedadmin.da.liveandadmin.hlx.pageEventApiResponseintypes/domain.ts— addedpageCreatedBy?: stringEVENT_DATA_FILTERinutils/dataFilters.ts— addedpageCreatedByassubmittable: trueso it survivesprepareEslEventPutPayloadTests
placeholders.test.ts,paths.test.ts,localization.test.ts— pure JS, no DOM/network mocks neededDocs
docs/DA_PAGE_CREATION_MIGRATION.md— step-by-step guide for ESL, events-service-platform, and events-platform-hh-webhooks teams; includes rollout order, verification checklists, and full-retirement cleanup tablesWhat this PR does NOT include
toast.errorvia the existingsaveErrorpath. A blocking per-locale retry dialog is a follow-up.useDaPageCreationexposesstatusandlocaleResultsbut no component consumes them yet.admin.da.liveandadmin.hlx.pageaccept the user's IMS token with CORS from the EMC origin before enabling for authors (see Risk R0 in the plan).docs/DA_PAGE_CREATION_MIGRATION.md; not implemented here.Test plan
npm run type-check— passes (zero errors)npx jest --testPathPattern="utils/daPage"— 67/67 passfetch('https://admin.da.live/source/adobecom/da-events/<scratch>/test.html', { method: 'GET', headers: { Authorization: 'Bearer <user IMS token>' }, mode: 'cors' })from the EMC dev origin — confirm CORS headers and token acceptancepageCreatedBy: 'emc'in the ESP event recordtoast.infoappears when DA page creation fails on a draft savemodified-by: EMCin the page's.metadatablock🤖 Generated with Claude Code