-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Discover] One Discover context awareness #183797
[Discover] One Discover context awareness #183797
Conversation
674a80c
to
a8f1b15
Compare
/ci |
1 similar comment
/ci |
src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
Outdated
Show resolved
Hide resolved
src/plugins/discover/public/context_awareness/profiles_manager.ts
Outdated
Show resolved
Hide resolved
return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { | ||
processRecord: (record) => { | ||
profilesCollector.collect({ record }); | ||
return record; | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was experimenting with optimizing records flattening by using a "lazy" getter function and I think it can become in handy for document context resolution too. We could add a "lazy" getter for this logic to the new DataTableRecord
class too. This way we would process records "on demand" and not all at once which should make things faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! Document context resolution is synchronous anyway, so a lazy getter should do the trick. I wouldn't expect the resolution process to be heavy in general, but it seems reasonable to avoid running it until necessary. I was able to implement it already, but it could certainly be improved if we switch to a class later: d8351ca#diff-8501e1b7df4eeb0b27b10309dd2ffbe366964c264334485dee5a1d404e51a583R104-R108.
484bb35
to
3a35c00
Compare
/ci |
|
||
setRootProfileLoading(true); | ||
|
||
profilesManager.resolveRootProfile({ solutionNavId }).then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: as we have async resolution of the profiles, should we handle errors here in case a profile resolution triggers one? Also the resolveRootProfile
method and the other resolution functions don't encapsulate the error, would be nice to also wrap the error there to something like ProfileResolutionError
than can be easily recognized and handled on this custom hook or where might be necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we should definitely handle errors in some way to avoid breaking the UI if profile resolution fails, although I lean toward handling this internally within ProfilesManager
, logging it, and resolving to the default profile on error to avoid having to handle it elsewhere in code. We could also surface resolution failures in the inspector when adding support for debugging context awareness. WDYT, or do you feel there's value in throwing an error and letting consuming code handle it instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm good with having the error handling centralized in ProfilesManager
as far as there are no surprising behaviours for the user when the profile resolution fails 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good! Added initial error handling and fallback logic here: faa68c2.
public registerProvider(provider: ProfileProvider<TProfile, TParams, TContext, TMode>) { | ||
this.providers.push(provider); | ||
this.providers.sort((a, b) => a.order - b.order); | ||
} | ||
|
||
public getProfile(context: ContextWithProfileId<TContext>): ComposableProfile<Profile> { | ||
const provider = this.providers.find((current) => current.profileId === context.profileId); | ||
return provider?.profile ?? EMPTY_PROFILE; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doubt: I might be missing something here, but could you explain a use case of where/why we need an order property for the registered profiles? The only case I can think of is in case we have the exact same resolved value in a specific part of the page (e.g. top nav items), but in this case shouldn't we enforce a constrained set of profileId
values to register a profile?
Taken from the example profile, where are we potentially going to register a profile with id logs-data-source-profile
more than once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order
prop determines the order for providers to run in during context resolution since the first match is the one that gets used. So for example if we have a logsDataSourceProfileProvider
and a metricsDataSourceProfileProvider
, the order
will determine which one tries to resolve first.
But this was done originally under the assumption we would expose the registration function externally, which may not be necessary if we just do all registrations internally in Discover. In that case, I think it would make sense to just run the providers in whichever order they were registered in code. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order prop determines the order for providers to run in during context resolution since the first match is the one that gets used. So for example if we have a logsDataSourceProfileProvider and a metricsDataSourceProfileProvider, the order will determine which one tries to resolve first.
This is the part where I have doubts, for the order
value to have a role in the resolution step, we are assuming that there might be multiple registration of profiles with id logsDataSourceProfileProvider
or anything else, what is the use case where we need to register multiple profiles for logs data source? Or is it just an example id and I am misleading the change that we can have profiles with same ids for other features?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bad explanation on my part, let me try again 😄
The order
property was used to determine the resolution order for different providers of the same type (e.g. RootProfileProvider
, DataSourceProfileProvider
, DocumentProfileProvider
), unrelated to profileId
, which as you would imagine should be unique. For example:
// Set order in providers
const logsDataSourceProfileProvider: DataSourceProfileProvider = { order: 0, profileId: 'logs-data-source-profile', ... };
const metricsDataSourceProfileProvider: DataSourceProfileProvider = { order: 100, profileId: 'metrics-data-source-profile', ... };
// `resolve` uses `order` to decide provider resolution order when looking for a matching profile
const context = await dataSourceProfileProvider.resolve({ ... });
The profileId
is used to map a resolved context to its corresponding profile so we can look it up later, e.g. to get the DocumentProfile
associated with a data table record:
// `getProfile` uses the `profileId` property of `record.context` to look up its profile
const documentProfile = documentProfileService.getProfile(record.context);
That said, the conversations I've had about it made me realize that order
is confusing and unnecessary assuming we intend to register providers from within the Discover codebase, in which case we can just rely on registration order, so I removed it here: e953b41.
3a35c00
to
a5f2de8
Compare
/ci |
1 similar comment
/ci |
5fb7971
to
5d7cdc3
Compare
/ci |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great foundation! LGTM 👍
|
||
public resolveDocumentProfile(params: DocumentProfileProviderParams) { | ||
Object.defineProperty(params.record, 'context', { | ||
get: memoize(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you help me understand why we're using a (memoized) property here instead of just storing a reference to the resolved document context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an optimization suggested in #183797 (comment) to limit the impact of context resolution on rendering until the document context is actually needed. In general I don't expect it to have much impact, but we could be handling up to 10,000 records here, and if any of the resolution methods do inner looping, the time complexity could grow quite quickly and become noticeable.
But when switching over to Proxy
, I ended up removing memoize
and just storing context
directly instead: 113f66f.
return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView); | ||
return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { | ||
processRecord: (record) => { | ||
services.profilesManager.resolveDocumentProfile({ record }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modifying the value in-place feels like a potential maintenance hazard. What's the argument for doing this instead of returning a wrapper or proxy of the record?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general I agree, but it felt less problematic here since we were modifying the record at creation time before anything else had access to it. I was mainly thinking about performance and allocations since this is running in a loop, but maybe that was premature and a Proxy
likely isn't too much overhead. I switched over to a Proxy
here, which also simplifies the ProfilesManager
a bit since there's no need to keep track of the latest record ID anymore: 113f66f.
I'm still not 100% on how I feel about this though. @jughosta it would be great to get your opinion on this too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's go with Proxy
solution 👍
id: String(idx), | ||
raw: row, | ||
raw: row as EsHitRecord, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be a dangerous type assertion? If, for example, the document context resolution relies on accessing one of the meta fields guaranteed by the EsHitRecord
(such as _index
), it would fail without the compiler recognizing the problem. One instance of this happening is the planned resolution heuristic in #184601.
Does this imply that we need to differentiate between hits and esql records in the context awareness mechanism?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a dangerous type assertion since ES|QL records only contain _id
and _index
if explicitly requested through the metadata
directive, but it's how ES|QL is implemented in Discover currently. It's mainly an issue of using EsHitRecord
in DataTableRecord
, which is used for both data views and ES|QL. We also do a similar cast for data view responses too, but it's safer there since we know _id
and _index
will be defined.
I think the best we can do for now without heavy refactoring is to ensure runtime safety by setting undefined _id
and _index
values to empty strings for ES|QL results, which I added here: 6607524.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the failing functional tests, this change affects the functionality. Maybe we should rather update the type of raw
in DataTableRecord
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that didn't work as intended 😅 I've reverted that commit now. I agree that the right way is to update the type, but I attempted that in this PR and it caused a cascade of type errors. Since this is an existing issue, I'd like to leave this as it is for this PR, but I will open a followup one that addresses the actual type issue. I just don't want to block other PRs that depend on this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened a PR based on this one for the type changes here: #184975. Let's see if it passes 🤞
@@ -51,7 +51,8 @@ export const LogAIAssistant = ({ | |||
return undefined; | |||
} | |||
|
|||
const message = doc.fields.find((field) => field.field === 'message')?.value[0]; | |||
const messageValue = doc.fields.find((field) => field.field === 'message')?.value; | |||
const message = Array.isArray(messageValue) ? messageValue[0] : messageValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean messageValue
can be something else instead of a JsonArray
? If so, can we somehow fix the type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this change because the logs overview tab was breaking for ES|QL records which may not have array values. But looking into it a bit deeper, the issue was actually in createLogsAIAssistantRenderer
, which was doing an unsafe cast to LogAIAssistantDocument['fields']
. I reverted this change and updated createLogsAIAssistantRenderer
to properly handle DataTableRecord
instead: 0bfbe8a.
export function buildDataTableRecordList<T extends DataTableRecord = DataTableRecord>( | ||
docs: EsHitRecord[], | ||
dataView?: DataView | ||
dataView?: DataView, | ||
{ processRecord }: { processRecord?: (record: DataTableRecord) => T } = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: as dataView
previously was the last optional argument, this function signature made sense. But if we add a parameter and invoke it without having a data view, we then need to force passing a second argument to pass then a third one, and it'll start looking something like
buildDataTableRecordList(docs, undefined, {processRecord})
Shall we refactor the function to get named parameters as we are adding the processRecord now?
buildDataTableRecordList({docs, dataView, processRecord})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree with this, but buildDataTableRecordList
is used outside of Discover and I'd like to avoid pinging other teams for review on this PR. I can make this change in a followup though to avoid blocking this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened a PR based on this one for the signature change here: #184976.
@@ -236,6 +242,11 @@ export function createDiscoverServicesMock(): DiscoverServices { | |||
contextLocator: { getRedirectUrl: jest.fn(() => '') }, | |||
singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, | |||
urlTracker: urlTrackerMock, | |||
profilesManager: new ProfilesManager( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doubt: should this be the same as the mock in https://github.com/elastic/kibana/pull/183797/files#diff-66364df80226d194a5368e2cc7208a0f29538806fb2865d46a82e8f97e1bf3ddR86 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be! Updated here: f5fd842.
await fetchEsql( | ||
{ esql: 'from *' }, | ||
dataViewWithTimefieldMock, | ||
discoverServiceMock.data, | ||
discoverServiceMock.expressions, | ||
{ requests: new RequestAdapter() }, | ||
discoverServiceMock.profilesManager | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning
This function is quickly growing and becomes more difficult to maintain and use, it might be a good time for a refactor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I guess it's about time we clean up this function's signature. Updated here: 3f1ba48.
src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts
Show resolved
Hide resolved
src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts
Show resolved
Hide resolved
@@ -34,6 +35,8 @@ const embeddableConfig = { | |||
executeTriggerActions, | |||
}; | |||
|
|||
services.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be a jest mock, so how about using .mockReturnValue()
to modify its behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not typed as one, so instead I just used jest.spyOn
: 2b8f6bf.
const { profilesManagerMock } = createContextAwarenessMocks(); | ||
|
||
servicesMock.profilesManager = profilesManagerMock; | ||
servicesMock.core.chrome.getActiveSolutionNavId$ = () => new BehaviorSubject('test'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be a jest mock, so how about using .mockReturnValue()
to modify its behavior?
const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); | ||
const cellRenderers = useMemo(() => { | ||
const getCellRenderers = getCellRenderersAccessor(() => customCellRenderer ?? {}); | ||
return getCellRenderers(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be misunderstanding, but isn't it necessary to get the cell renderer for each row with the corresponding record
instead of just once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unified Data Table cell renders are column based currently, so they don't work on individual records and can't be supported by DocumentProfile
right now. I'm not sure if we need cell renderers on a per-record basis yet, but maybe there are some good use cases for it.
If we decide we need it, I'd recommend we add support for them and do the necessary Unified Data Table refactoring in a followup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry, I didn't know this was an intentional omission. Could this indicate that narrowing the composable profile interface by context type might be good? Because I would also have made that mistake when trying to implement a custom cell renderer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that would make sense and it's actually already supported, I just forgot to omit getCellRenderers
from the DocumentProfile
type when I reverted the example extension points. Updated it here for now so it will be enforced in providers, and we can revisit this later if we find a need for document specific cell renderers: 8bab582.
@@ -338,11 +339,14 @@ export function DiscoverMainRoute({ | |||
stateContainer, | |||
]); | |||
|
|||
const { solutionNavId } = customizationContext; | |||
const { rootProfileLoading } = useRootProfile({ solutionNavId }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Logs Explorer doesn't seem to pick up the root context from the solution nav. I guess we need to call the root resolution step separately there? This would be true for every consumer of the <DiscoverContainer>
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's a good point, although I think I'd like to avoid supporting profiles in DiscoverContainer
anyway if possible. I could change useRootProfile
to listen for core.chrome.getActiveSolutionNavId$
instead, but what if we just pass an empty ProfilesManager
for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although I think I'd like to avoid supporting profiles in DiscoverContainer anyway if possible.
Does this mean those apps embedding the DiscoverContainer (such as LogsExplorer) won't benefit from the resolution logic built-in in Discover with the new data source and document profile providers?
Isn't it the purpose of making Discover aware of the data so that the other solutions embedding it get the ad-hoc behaviours out of the box?
Let me know if I misunderstood this and what is the intended support here please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd agree with @tonyghiani here. For any future addition to the Logs Explorer we'd all benefit if we could instead add it to a matching Discover context.
What would be the downside of having the Logs Explorer explicitly resolve the root context (always to an obs root context)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean those apps embedding the DiscoverContainer (such as LogsExplorer) won't benefit from the resolution logic built-in in Discover with the new data source and document profile providers?
Isn't it the purpose of making Discover aware of the data so that the other solutions embedding it get the ad-hoc behaviours out of the box?
It does mean that, but I've been under the impression the purpose of the contextual experience in Discover was to avoid the need for embedding Discover into solution apps, and instead have a single Discover app that can be linked to from within solutions. We discussed this in the engineering sync earlier today and I understand it's contentious, but I really think this is something that needs alignment by product across platform and solutions so engineering has a clear understanding of the product intentions.
I have a meeting tomorrow with my PM and director and hopefully will have a better understanding of the situation after. I personally have no problem supporting this if it's aligned with the vision since I don't think it's technically difficult, but if we don't have clarity on it after tomorrow's meeting, I'd really prefer if we merge this PR without supporting the embedded experience to avoid blocking progress until we are aligned at the product level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarity and managing the communication with PM/Director Davis!
I have clear the vision for this and the future of One Discover, and your reasoning makes sense.
On the other hand, it's even true there are still many differences between Discover and the embedded version, which means we have to maintain the same features in different parts of the codebase if we don't rely on a single mechanism.
For instance, we did some work to bring the Logs overview into the flyout so that every log user could use it based on the resolved context. What feels counter-productive is having vanilla Discover as the only app taking decisions based on context (enabling the flyout overview tab in this case), while also have in parallel the embedded version manually enable the same features (logs explorer should keep manually allowing that flyout to tab until we have feature parity and Logs Explorer can be removed).
The above would apply to other features and implementation could quickly diverge in how things are done until we have a good level of feature parity and we keep a One Discover.
If is a minimal effort, I would strongly opt for a single source of truth to resolve specific customizations, which would be the profile resolution we have worked on so far.
Let me know your thoughts, I'm happy to talk more about this if needed (I missed the engineering sync yesterday, not sure how in depth this was already covered, I'll check the recording)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the additional context @tonyghiani. I discussed this with my PM and director Friday, and we'd like to avoid supporting the profiles system in DiscoverContainer
, at least for the time being until product teams across Platform and O11y are aligned.
The main concern is that the current Discover logs experience PRD diverges significantly from what exists in Logs Explorer, and it's not clear if these will actually converge at some point. Depending on product decisions currently being discussed, parity between the Discover logs experience and Logs Explorer may not actually be a goal.
If we support the profiles system for DiscoverContainer
, it means Logs Explorer will automatically inherit the Discover logs profile enhancements, which isn't something we want currently. We also don't want to migrate current Logs Explorer features to the Discover logs profile unless it's decided to include them as part of the PRD.
I also don't want to maintain parallel systems with the customization framework and the contextual awareness framework since it creates tech debt, but I think this is the right way to move forward for now until we have more clarity.
This isn't me saying no it won't happen (especially since it's not my decision to make), just that I'd like to move forward with this PR without DiscoverContainer
support, and work with our product teams to reach alignment on if it should be supported. I think this would be a good topic for our weekly One Discover sync, and we know it's technically easy to change if needed.
if (isEqual(this.prevRootProfileParams, serializedParams)) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am curious about the need of storing the previous reference to params and diffing the params here?
Imo this resolve function should be dumb and should simply resolve the profile. And since this function will always be called from react
( current me if assumption is wrong ), we should make use of that reactivity to call resolveRootProfile
only when params
have changed. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely get where you're coming from, but there are two reasons I did it this way:
- The comparison logic is pretty simple right now and could probably just use a React deps array, but it could become more complex over time. E.g. we're currently re-resolving data source context whenever the ES|QL query changes, but we may later decide it's only necessary when the actual index pattern changes, so I think having a central place for this logic outside of React can be helpful.
- The resolve methods are actually mostly called from outside of React, in the state container and the saved search embeddable, so keeping it all in React would be tricky.
But the resolve methods in the actual profile services are dumb and just run through the list of providers looking for a match.
[TKey in keyof TProfile]?: ComposableAccessor<TProfile[TKey]>; | ||
}; | ||
|
||
export const getMergedAccessor = <TKey extends keyof Profile>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be worth adding a couple of lines of JSDoc as it is not 100% clear when this utilities do exactly from the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally agree, but I'd prefer to do this as part of the dev documentation issue I have as a followup here: #184698.
import { useProfiles } from './use_profiles'; | ||
import type { Profile } from '../types'; | ||
|
||
export const useProfileAccessor = <TKey extends keyof Profile>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as before.
I think it might be worth adding a couple of lines of JSDoc as it is not 100% clear when this utilities do exactly from the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the limitations that we don't have real profile properties to test it with, this looks ok to me. Good job and thanks for the thorough description.
Maybe the follow-up documentation could point out the mismatch between how Hit
s and ES|QL rows work in the document context resolution - and provide some guidance on how to handle it.
}); | ||
|
||
it('should log an error and fall back to the default profile if root profile resolution fails', async () => { | ||
await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not saying this needs to change now, but I found these tests quite difficult to read. Contributing factors might have been:
- The error is provoked by monkey-patching a method instead of using a profile provider that fails "naturally".
- The
addLog
is an unfortunate side-effect on its own, but also using it to test formatted error messages doubles the smell.
A cleaner solution could be to decompose it into layers responsible for the cache, the async side effects and the reactivity, which could be tested separately. But I recognize that would be disproporionate effort it this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback, and I'm certainly open to followup improvements (although as you mentioned, I'd prefer to limit changes at this point for the sake of unblocking dependent work). This feedback makes sense to me overall, but I'm a bit confused on this point:
The
addLog
is an unfortunate side-effect on its own, but also using it to test formatted error messages doubles the smell.
Is your concern here the use of the static addLog
function for logging, or that ProfilesManager
handles this logging itself? The way I interpreted this is that you feel it would be cleaner to do something along the lines of returning or throwing an error on resolution failure, and logging it externally, rather than logging as a side effect of resolution failure, but I'm not sure if that's what you meant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's exactly what I meant. But if we want to keep it in there for convenience, maybe an improvement would be to supply the logging side-effect to the profile manager as a constructor argument? 🤔
GitHub counts my review as sufficient for the Obs UX Logs team, but I'd appreciate it if @tonyghiani got a chance to follow up on his comments too. 😇 |
💚 Build Succeeded
Metrics [docs]Module Count
Public APIs missing comments
Async chunks
Page load bundle
Unknown metric groupsAPI count
ESLint disabled in files
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: cc @davismcphee |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davismcphee Thanks for this piece of work and for patiently addressing all my doubts/suggestions, this is a solid foundation to handle profiles in Discover from now on! 👏
Summary
This PR includes the initial implementation of the Discover contextual awareness framework and composable profiles:
Context
We currently support three levels of context in Discover:
Composable profiles
To support application extensibility based on context, we've introduced the concept of "composable profiles". Composable profiles are implementations of a core
Profile
interface (or a subset of it) containing all of the available extension points Discover supports. A composable profile can be implemented at any context level through a "profile provider", responsible for defining the composable profile and its associated context resolution method. The context resolution method, namedresolve
, determines if its composable profile is a match for the current Discover context, and returns related metadata in acontext
object.Merged accessors
Composable profiles operate similarly to middleware in that each of their extension point implementations are passed a
prev
argument, which can be called to access the results from profiles at previous context levels, and allows overwriting or composing a final result from the previous results. The method Discover calls to trigger the extension point merging process and obtain a final result from the combined profiles is referred to as a "merged accessor".The following diagram illustrates the extension point merging process:
Supporting services
The contextual awareness framework is driven by two main supporting services called
ProfileService
andProfilesManager
.Each context level has a dedicated profile service, e.g.
RootProfileService
, which is responsible for accepting profile provider registrations and running through each provider in order during context resolution to identify a matching profile.A single
ProfilesManager
is instantiated on Discover load, or one per saved search panel in a dashboard. The profiles manager is responsible for the following:Bringing it all together
The following diagram models the overall Discover contextual awareness framework and how each of the above concepts come together:
Followup work
Testing notes
Testing the framework is tricky since we have no actual profile or extension point implementations yet. However, I previously added example implementations that I was using for testing while building the framework. I've removed the example implementations so they don't get merged, but they can be temporarily restored for testing by reverting the commit where I removed them:
git revert 5752651f474d99dfbdecfe9d869377b9edaf7c62
.You'll also need to uncomment the following lines in
src/plugins/discover/public/plugin.tsx
:kibana/src/plugins/discover/public/plugin.tsx
Lines 458 to 463 in ce85a6a
To test the root profile resolution based on solution type, I'd recommend enabling the solution nav locally by adding the following to
kibana.dev.yml
:In order to change the active solution type, modify the
mockSpaceState
insrc/plugins/navigation/public/plugin.tsx
:kibana/src/plugins/navigation/public/plugin.tsx
Lines 159 to 162 in 79e51d6
For test data, I'd recommend running the following commands to generate sample ECS compliant logs and metrics data:
And lastly a couple of the ES|QL queries I used for testing:
Resolves #181962.
Checklist
For maintainers