Skip to content

Add page feedback collection#3705

Open
reakaleek wants to merge 9 commits into
mainfrom
helix-chiller
Open

Add page feedback collection#3705
reakaleek wants to merge 9 commits into
mainfrom
helix-chiller

Conversation

@reakaleek

@reakaleek reakaleek commented Jul 23, 2026

Copy link
Copy Markdown
Member

Why

Documentation teams need page-level signals to identify content gaps and prioritize improvements without requiring readers to leave the page.

What

Adds an animated feedback control for standard Markdown and API reference pages in Assembler and Codex builds. Reactions are captured optimistically, optional comments can be retried, and users can revise or revoke their response within the page view.

Feedback is stored through new API endpoints in an environment-specific Elasticsearch index. The mapping is source-generated and bootstrapped at API startup with Elastic.Ingest.Elasticsearch. The feature remains flag-controlled, is disabled for air-gapped builds, and is enabled for dev and staging.

image
Screen.Recording.2026-07-23.at.14.49.25.mov

How

A lazily registered React web component submits a client-generated feedback ID through PUT and DELETE endpoints. The API uses a dedicated Elasticsearch transport and hash-aware startup bootstrap so mapping templates stay aligned with the serialized document model.

Test plan

  • Run page feedback frontend tests
  • Run dotnet test tests/Elastic.Documentation.Api.Tests/
  • Build Elastic.Documentation.Site
  • Publish the API with Native AOT

Made with Cursor

Capture per-page reactions and optional comments so documentation teams can identify content gaps and prioritize improvements.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep failed comments visible so users can retry from the existing form without a detached recovery action.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@reakaleek
reakaleek marked this pull request as draft July 23, 2026 09:37
Remove retry-specific indirection now that failed comments remain in the existing form.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@theletterf

Copy link
Copy Markdown
Member

+CC @technige A good candidate for the "Other" slide :)

Capture reactions promptly while collecting queryable reasons and optional details in a changeable, retry-safe flow.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@reakaleek
reakaleek marked this pull request as ready for review July 23, 2026 13:00
@reakaleek
reakaleek marked this pull request as draft July 23, 2026 13:01

@Mpdreamz Mpdreamz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good first pass — the feature shape is right. Four concrete follow-ups below.

Pre-existing note (ElasticsearchAskAiMessageFeedbackGateway.cs:71): record.Reaction.ToString().ToLowerInvariant() bypasses whatever [JsonStringEnumMemberName] attributes the reaction enum declares. Worth fixing alongside the PageFeedbackDocument change.

FeedbackId = record.FeedbackId.ToString(),
PageUrl = record.PageUrl,
PageTitle = record.PageTitle,
Reaction = record.Reaction == PageFeedbackReaction.ThumbsUp ? "thumbsUp" : "thumbsDown",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageFeedbackReaction and PageFeedbackReason already declare [JsonStringEnumMemberName] with the correct camelCase values. Store the enum types directly on PageFeedbackDocument and let the serializer do the mapping:

// PageFeedbackDocument
[Keyword]
[JsonPropertyName("reaction")]
public required PageFeedbackReaction Reaction { get; init; }

[Keyword]
[JsonPropertyName("reason")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public PageFeedbackReason? Reason { get; init; }

Register the enum types in PageFeedbackJsonContext, then here:

Reaction = record.Reaction,
Reason = record.Reason,

GetReasonValue disappears. The serializer writes "thumbsUp" / "solvedProblem" etc. from the attributes — no separate mapping to maintain.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e56fe23 — PageFeedbackDocument now stores the enum types directly, registers them with the source-generated JSON context, and removes the duplicate string mapping.


try
{
var json = JsonSerializer.Serialize(document, PageFeedbackJsonContext.Default.PageFeedbackDocument);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.51.0 added DirectWriteAsync for exactly this use-case: a single-document write in an API handler that needs to await persistence before returning. Register IngestChannel<PageFeedbackDocument> as a DI singleton (sharing the MappingContext that bootstrap already uses) and inject it here:

var response = await channel.DirectWriteAsync(
    new[] { document },
    retries: 2,
    cancellationToken: ctx);

if (!response.AllItemsPersisted())
{
    if (response.TryGetServerErrorReason(out var reason))
        logger.LogWarning("Failed to index page feedback {FeedbackId}: {Reason}", record.FeedbackId, reason);
    return false;
}
return true;

Eliminates the manual URL construction, JsonSerializer.Serialize, PostData.String, and the StringResponse cast.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a54536e — page feedback upserts now use DirectWriteAsync and verify AllItemsPersisted before returning. I omitted the channel retry overload intentionally because the browser already owns the bounded retry behavior for this endpoint.

{
public async Task StartAsync(CancellationToken cancellationToken)
{
var options = new IngestChannelOptions<PageFeedbackDocument>(transport.Transport, index.MappingContext);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If IngestChannel<PageFeedbackDocument> is registered as a DI singleton (as suggested on the gateway), inject it here instead of creating a transient channel just for bootstrap. Both the bootstrap service and the gateway then share the same channel instance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a54536e — IngestChannel is now a DI singleton shared by PageFeedbackBootstrapService and ElasticsearchPageFeedbackGateway.

internal sealed partial class PageFeedbackJsonContext : JsonSerializerContext;

[ElasticsearchMappingContext(JsonContext = typeof(PageFeedbackJsonContext))]
[Index<PageFeedbackDocument>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add MappingVersionFromAssembly = true for version-aware bootstrap guards:

[Index<PageFeedbackDocument>(
    NameTemplate = "page-feedback-v1-{env}",
    Dynamic = false,
    MappingVersionFromAssembly = true
)]

The generated code reads <Version> from the assembly and stores it in _meta.mapping_version on the template. During a rolling deploy, a pod still on the older version will skip bootstrap instead of overwriting the newer template with older mappings. Docs: https://elastic.github.io/elastic-ingest-dotnet/strategies/bootstrap/#version-aware-bootstrap-guards

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a54536e — the latest commit added MappingVersionFromAssembly = true and an assertion that the generated mapping version matches the API assembly version.

Comment thread src/Elastic.Codex/_MarkdownLayout.cshtml
Comment thread src/Elastic.ApiExplorer/_Layout.cshtml
Use direct channel writes and assembly mapping versions so API responses reflect item persistence and rolling deployments cannot downgrade templates.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…dreamz)

Consolidate endpoint transport creation in configuration so API feedback adapters reuse one connection pool and tooling no longer maintains duplicate factories.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the enum wire-name attributes as the single source of truth so stored values cannot drift from the API contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
@shainaraskas

Copy link
Copy Markdown
Member

does this feedback get exposed to writers?

@reakaleek

Copy link
Copy Markdown
Member Author

does this feedback get exposed to writers?

That's the goal

Keep the feedback prompt visually separated from both page content and navigation.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@Mpdreamz Mpdreamz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we introduce rate limiting to the API at this point?

https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-10.0

Should we add cloudflare turnstile?

@reakaleek

Copy link
Copy Markdown
Member Author

Should we introduce rate limiting to the API at this point?

https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-10.0

Should we add cloudflare turnstile?

We have a general rate limit in WAF. But it's definitely worth double-checking in docs-infra.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants