Add page feedback collection#3705
Conversation
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>
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>
|
+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>
Mpdreamz
left a comment
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Fixed in a54536e — the latest commit added MappingVersionFromAssembly = true and an assertion that the generated mapping version matches the API assembly version.
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>
|
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
left a comment
There was a problem hiding this comment.
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. |
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.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
dotnet test tests/Elastic.Documentation.Api.Tests/Elastic.Documentation.SiteMade with Cursor