Analytics components from query handlers
Your search engine returns 20 product IDs plus per-product context: a tracking token (attributionToken) the vendor wants back if the customer clicks, the position in the result set, an internal relevance score. That data only exists in the search response. There is no getAttributionTokenForProduct(id) endpoint, because the token is generated per-search and tied to that one user's session.
You still want the data on the storefront. Most often: the attribution token has to ride along on the product detail page link as a query parameter so the search vendor can attribute the click back to the right impression.
The pattern: provide a single entity component from the query handler inline, alongside whatever the regular product component resolvers do. Skip writing a resolver for the analytics component, because there is no source of truth for it outside this one query.
import { ProductAnalytics } from '@laioutr-core/canonical-types/entity/product';
export default defineMyAppQuery({
implements: ProductRecommendationsQuery,
provides: [ProductAnalytics], // ← only the analytics component
run: async ({ context, $entity }) => {
const response = await context.searchClient.recommend({ /* ... */ });
return {
entities: response.results.map((result) =>
$entity({
id: result.productId,
analytics: () => ({
pdpLinkQuery: { attributionToken: response.attributionToken },
}),
}),
),
};
},
});
The Shopify, Shopware, or other Product component resolvers continue providing base, prices, media, and everything else they normally do. The query handler only contributes the one component it has data for.
How this combines with component resolvers
When the frontend requests, say, [ProductBase, ProductPrices, ProductAnalytics] against this query:
ProductAnalyticsis inprovides:, so Orchestr extracts it from the query handler's inlineentitiesarray and returns it directly.ProductBaseandProductPricesare not inprovides:, so Orchestr forwards them to the registered Product component resolvers, just like it would for any other Product entity.
The two paths run in parallel for each entity. The frontend gets one assembled Product with all three components, and never knows the analytics piece came from a different source than the rest.
Why no resolver
Component resolvers are how Orchestr fetches a component for an entity given only the entity ID. They run anywhere a Product is needed: by-slug query, by-id query, category listings, link targets. If you wrote a resolver for ProductAnalytics, Orchestr would call it from those contexts too. The resolver would have nothing to return because the analytics data only exists in the originating query's response.
So the contract is: analytics is undefined for a Product fetched by anything other than a search-style query. The frontend handles that gracefully.
<script setup lang="ts">
const props = defineProps<{ product: ProductWithComponents }>();
const detailLink = computed(() => {
const base = `/products/${props.product.components.base.slug}`;
const params = props.product.components.analytics?.pdpLinkQuery;
if (!params) return base;
return `${base}?${new URLSearchParams(params).toString()}`;
});
</script>
When this pattern fits
Search results, AI recommendations, "frequently bought together", "you might also like", manually merchandised collections with vendor-tracked impressions: anywhere the upstream returns per-result context the standalone product lookup couldn't reconstruct.
When to write a resolver instead
If the component data has a real source you can query independently (a per-product loyalty tier, a stock-availability lookup, a CMS-managed badge), write a resolver for it instead. The inline-from-query path is for data that genuinely doesn't exist outside the query that produced it.
Related
- Queries: Inline Entity Data: the underlying mechanism
provides:builds on. - Component resolvers: when a component does have a standalone source and should get a resolver.
Recipes
Patterns and gotchas from building connectors against real commerce backends. Each recipe captures one problem we hit and the approach that worked.
Identity cookies (cart and visitor IDs)
Read-or-create-and-set patterns for cart, session, and visitor identity cookies. Where to put the bootstrap depends on when you need the ID.