fix(memory): make PgSQL the single source of truth for user entity al…#896
Merged
keeees merged 2 commits intorelease/v0.3.0from Apr 14, 2026
Merged
fix(memory): make PgSQL the single source of truth for user entity al…#896keeees merged 2 commits intorelease/v0.3.0from
keeees merged 2 commits intorelease/v0.3.0from
Conversation
…iases - Skip alias merging for user entities during dedup (_merge_attribute and _merge_entities_with_aliases) to prevent dirty data from overwriting PgSQL authoritative aliases - Add PgSQL→Neo4j alias sync after Neo4j write in write_tools to ensure Neo4j user entities always reflect the PgSQL source - Remove deduped_aliases (Neo4j history) from alias sync in extraction_orchestrator, only append newly extracted aliases to PgSQL - Guard Neo4j MERGE cypher to preserve existing aliases for user entities (name IN ['用户','我','User','I']) - Fix emotion_analytics_service query to use ExtractedEntity label and entity_type property
Contributor
Reviewer's Guide此 PR 将 PostgreSQL 作为用户别名(user aliases)的权威数据源,并调整了抽取、去重逻辑、Neo4j 写入路径和分析查询,使 Neo4j 不再向 PgSQL 回写别名,而是由 PgSQL 同步用户实体到 Neo4j,同时修复了一个实体查询中的标签/属性不匹配问题。 写入路径中 PgSQL→Neo4j 用户别名同步的时序图sequenceDiagram
actor Client
participant WriteService as write_tools_write
participant PgSQL as PgSQL_end_user_info
participant Neo4j as Neo4j_ExtractedEntity
participant Celery as Celery_clustering_task
Client->>WriteService: write(all_entity_nodes, memory_config, ...)
WriteService->>Neo4j: execute_query(save ExtractedEntity nodes)
Neo4j-->>WriteService: success
alt all_entity_nodes not empty
WriteService->>WriteService: end_user_id = all_entity_nodes[0].end_user_id
opt end_user_id exists
WriteService->>PgSQL: EndUserInfoRepository.get_by_end_user_id(end_user_id)
PgSQL-->>WriteService: info.aliases as pg_aliases
alt pg_aliases not empty
WriteService->>Neo4j: MATCH ExtractedEntity WHERE end_user_id = end_user_id AND name in 用户/我/User/I SET aliases = pg_aliases
Neo4j-->>WriteService: aliases updated
else pg_aliases empty
WriteService->>WriteService: skip alias sync
end
end
WriteService->>Celery: run_incremental_clustering.apply_async(...)
Celery-->>WriteService: task_id
else no entities
WriteService->>WriteService: skip alias sync and clustering
end
WriteService-->>Client: write result
从当前对话抽取结果更新 PgSQL 用户别名的时序图sequenceDiagram
participant Engine as ExtractionEngine
participant Orchestrator as extraction_orchestrator
participant PgSQL as PgSQL_end_user_info
participant Neo4j as Neo4j_ExtractedEntity
Engine->>Orchestrator: _update_end_user_other_name(entity_nodes, dialog_data_list, end_user_id)
Orchestrator->>Orchestrator: current_aliases = _extract_current_aliases(entity_nodes, dialog_data_list)
Orchestrator->>Neo4j: _fetch_neo4j_assistant_aliases(end_user_id)
Neo4j-->>Orchestrator: neo4j_assistant_aliases
Orchestrator->>Orchestrator: filter current_aliases by assistant aliases
alt current_aliases empty
Orchestrator-->>Engine: return (no update)
else
Orchestrator->>PgSQL: EndUserRepository.get(end_user_id)
PgSQL-->>Orchestrator: end_user
alt end_user not found
Orchestrator-->>Engine: return
else
Orchestrator->>PgSQL: EndUserInfoRepository.get_by_end_user_id(end_user_id)
PgSQL-->>Orchestrator: info.aliases as db_aliases
Orchestrator->>Orchestrator: filter placeholder names in db_aliases
Orchestrator->>Orchestrator: merged_aliases = db_aliases + current_aliases (dedup, keep order)
alt info exists
Orchestrator->>PgSQL: update EndUserInfo.aliases = merged_aliases
else
Orchestrator->>PgSQL: insert EndUserInfo with aliases = merged_aliases
end
Orchestrator-->>Engine: done
end
end
文件级改动
Tips and commands与 Sourcery 交互
自定义使用体验打开你的 dashboard 来:
获取帮助Original review guide in EnglishReviewer's GuideThis PR makes PostgreSQL the authoritative source of user aliases and adjusts the extraction, deduplication, Neo4j write path, and analytics query so that Neo4j no longer feeds aliases back into PgSQL, and user entities in Neo4j are synchronized from PgSQL instead, while fixing an entity query label/property mismatch. Sequence diagram for PgSQL→Neo4j user alias synchronization in write pathsequenceDiagram
actor Client
participant WriteService as write_tools_write
participant PgSQL as PgSQL_end_user_info
participant Neo4j as Neo4j_ExtractedEntity
participant Celery as Celery_clustering_task
Client->>WriteService: write(all_entity_nodes, memory_config, ...)
WriteService->>Neo4j: execute_query(save ExtractedEntity nodes)
Neo4j-->>WriteService: success
alt all_entity_nodes not empty
WriteService->>WriteService: end_user_id = all_entity_nodes[0].end_user_id
opt end_user_id exists
WriteService->>PgSQL: EndUserInfoRepository.get_by_end_user_id(end_user_id)
PgSQL-->>WriteService: info.aliases as pg_aliases
alt pg_aliases not empty
WriteService->>Neo4j: MATCH ExtractedEntity WHERE end_user_id = end_user_id AND name in 用户/我/User/I SET aliases = pg_aliases
Neo4j-->>WriteService: aliases updated
else pg_aliases empty
WriteService->>WriteService: skip alias sync
end
end
WriteService->>Celery: run_incremental_clustering.apply_async(...)
Celery-->>WriteService: task_id
else no entities
WriteService->>WriteService: skip alias sync and clustering
end
WriteService-->>Client: write result
Sequence diagram for updating PgSQL user aliases from current dialog extractionsequenceDiagram
participant Engine as ExtractionEngine
participant Orchestrator as extraction_orchestrator
participant PgSQL as PgSQL_end_user_info
participant Neo4j as Neo4j_ExtractedEntity
Engine->>Orchestrator: _update_end_user_other_name(entity_nodes, dialog_data_list, end_user_id)
Orchestrator->>Orchestrator: current_aliases = _extract_current_aliases(entity_nodes, dialog_data_list)
Orchestrator->>Neo4j: _fetch_neo4j_assistant_aliases(end_user_id)
Neo4j-->>Orchestrator: neo4j_assistant_aliases
Orchestrator->>Orchestrator: filter current_aliases by assistant aliases
alt current_aliases empty
Orchestrator-->>Engine: return (no update)
else
Orchestrator->>PgSQL: EndUserRepository.get(end_user_id)
PgSQL-->>Orchestrator: end_user
alt end_user not found
Orchestrator-->>Engine: return
else
Orchestrator->>PgSQL: EndUserInfoRepository.get_by_end_user_id(end_user_id)
PgSQL-->>Orchestrator: info.aliases as db_aliases
Orchestrator->>Orchestrator: filter placeholder names in db_aliases
Orchestrator->>Orchestrator: merged_aliases = db_aliases + current_aliases (dedup, keep order)
alt info exists
Orchestrator->>PgSQL: update EndUserInfo.aliases = merged_aliases
else
Orchestrator->>PgSQL: insert EndUserInfo with aliases = merged_aliases
end
Orchestrator-->>Engine: done
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并留下了一些整体性的反馈:
- 用户占位名的特殊处理在多个模块中存在重复(例如
_USER_PLACEHOLDER_NAMES,以及在 Cypher 和 write_tools 中硬编码的['用户','我','User','I']);建议把这份列表集中到一个共享常量中,以避免后续出现不一致。 - 在
write_tools.write中,end_user_id是直接从all_entity_nodes[0]取得的,没有校验所有节点是否共享相同的end_user_id;你可能需要加断言或校验其一致性,以避免在边缘场景中把别名同步到错误的用户。 - PgSQL→Neo4j 的别名同步只在
pg_aliases非空时才会运行,这意味着一旦 Neo4j 中的别名被设置,就无法被清空;如果“清空别名”是一个合法场景,建议显式处理空列表的情况,而不是直接跳过。
给 AI Agents 的提示
Please address the comments from this code review:
## Overall Comments
- 用户占位名的特殊处理在多个模块中存在重复(例如 `_USER_PLACEHOLDER_NAMES`,以及在 Cypher 和 write_tools 中硬编码的 `['用户','我','User','I']`);建议把这份列表集中到一个共享常量中,以避免后续出现不一致。
- 在 `write_tools.write` 中,`end_user_id` 是直接从 `all_entity_nodes[0]` 取得的,没有校验所有节点是否共享相同的 `end_user_id`;你可能需要加断言或校验其一致性,以避免在边缘场景中把别名同步到错误的用户。
- PgSQL→Neo4j 的别名同步只在 `pg_aliases` 非空时才会运行,这意味着一旦 Neo4j 中的别名被设置,就无法被清空;如果“清空别名”是一个合法场景,建议显式处理空列表的情况,而不是直接跳过。
## Individual Comments
### Comment 1
<location path="api/app/core/memory/agent/utils/write_tools.py" line_range="204-209" />
<code_context>
+ with get_db_context() as db_session:
+ info = EndUserInfoRepository(db_session).get_by_end_user_id(uuid.UUID(end_user_id))
+ pg_aliases = info.aliases if info and info.aliases else []
+ if pg_aliases:
+ await neo4j_connector.execute_query(
+ """
+ MATCH (e:ExtractedEntity)
+ WHERE e.end_user_id = $end_user_id AND e.name IN ['用户', '我', 'User', 'I']
+ SET e.aliases = $aliases
+ """,
+ end_user_id=end_user_id, aliases=pg_aliases,
</code_context>
<issue_to_address>
**issue (bug_risk):** 请考虑在 PgSQL 别名列表为空时,同步清空 Neo4j 中对应用户实体的别名。
由于当前查询只在 `pg_aliases` 非空时执行,因此任何在 PgSQL 中“有意清空别名”的操作(例如用户主动删除数据、清理任务)都不会反映到 Neo4j 中,从而留下过期的别名。为了确保 Neo4j 以 PgSQL 为权威数据源,建议在 `info` 存在时始终执行该查询,并在 `pg_aliases` 为空时,将 `e.aliases` 设置为空列表(或根据你的约定设置为 `NULL`)。
</issue_to_address>
### Comment 2
<location path="api/app/core/memory/agent/utils/write_tools.py" line_range="207-208" />
<code_context>
+ if pg_aliases:
+ await neo4j_connector.execute_query(
+ """
+ MATCH (e:ExtractedEntity)
+ WHERE e.end_user_id = $end_user_id AND e.name IN ['用户', '我', 'User', 'I']
+ SET e.aliases = $aliases
+ """,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 建议避免在 Cypher 中硬编码用户占位名,以减少与 Python 侧占位名集合漂移的风险。
此处的 `IN ['用户', '我', 'User', 'I']` 过滤条件与 Python 中定义的占位名集合(`_USER_PLACEHOLDER_NAMES` / `USER_PLACEHOLDER_NAMES`)形成了重复。如果这两者出现不一致,一些用户实体可能无法再收到别名更新。请考虑将 Python 中的占位名列表作为参数传入该查询,或改为使用 Python 和 Cypher 共同依赖的共享配置。
建议实现:
```python
if pg_aliases:
await neo4j_connector.execute_query(
"""
MATCH (e:ExtractedEntity)
WHERE e.end_user_id = $end_user_id AND e.name IN $user_placeholder_names
SET e.aliases = $aliases
""",
end_user_id=end_user_id,
aliases=pg_aliases,
user_placeholder_names=USER_PLACEHOLDER_NAMES,
)
logger.info(f"[AliasSync] Neo4j 用户实体 aliases 已用 PgSQL 权威源覆盖: {pg_aliases}")
```
1. 确保 `USER_PLACEHOLDER_NAMES`(或对应的常量)在该模块中已被导入或可用:
- 如果在本文件中定义,请确认其命名为 `USER_PLACEHOLDER_NAMES`。
- 如果定义在其他位置(例如 `app.core.memory.agent.constants`),请在文件顶部增加如下导入:
`from app.core.memory.agent.constants import USER_PLACEHOLDER_NAMES`。
2. 如果规范的常量名称是 `_USER_PLACEHOLDER_NAMES`,则可以:
- 直接导入并起别名为 `USER_PLACEHOLDER_NAMES`,或者
- 将参数值改为实际名称(例如 `user_placeholder_names=_USER_PLACEHOLDER_NAMES`),其余保持不变。
</issue_to_address>
### Comment 3
<location path="api/app/core/memory/storage_services/extraction_engine/deduplication/deduped_and_disamb.py" line_range="91-98" />
<code_context>
- unique_aliases.append(alias_stripped)
+ # 收集所有需要合并的别名
+ all_aliases = list(getattr(canonical, "aliases", []) or [])
+ if incoming_name and incoming_name != canonical_name:
+ all_aliases.append(incoming_name)
+ all_aliases.extend(getattr(ent, "aliases", []) or [])
- # 排序并赋值
- canonical.aliases = sorted(unique_aliases)
+ try:
+ from app.core.memory.utils.alias_utils import normalize_aliases
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 建议在合并别名时过滤掉用户占位名,避免污染非用户实体的别名集合。
在当前合并逻辑中,只要 `canonical_name` 不在 `_USER_PLACEHOLDER_NAMES` 中,就会把来自 `ent` 的别名(包括 `incoming_name`)添加到 `canonical`。如果一个带有用户占位名(例如“我”/“User”)的实体被合并进一个非用户的 canonical 实体,那么该占位名就会被存储为该实体的别名,这与“用户别名仅通过 PgSQL 管理”的设计意图相冲突。为避免这一问题,请在处理 `incoming_name` 和 `ent.aliases` 时,跳过所有出现在 `_USER_PLACEHOLDER_NAMES` 中的别名(不区分大小写)。
```suggestion
# 收集所有需要合并的别名
all_aliases = list(getattr(canonical, "aliases", []) or [])
# 过滤掉用户占位名,避免污染非用户实体的别名集合(对 incoming_name)
if incoming_name and incoming_name != canonical_name:
incoming_name_stripped = incoming_name.strip()
if incoming_name_stripped and incoming_name_stripped.lower() not in _USER_PLACEHOLDER_NAMES:
all_aliases.append(incoming_name_stripped)
# 过滤掉用户占位名,避免污染非用户实体的别名集合(对 ent.aliases)
ent_aliases = getattr(ent, "aliases", []) or []
filtered_ent_aliases = []
for alias in ent_aliases:
if not isinstance(alias, str):
continue
alias_stripped = alias.strip()
if not alias_stripped:
continue
if alias_stripped.lower() in _USER_PLACEHOLDER_NAMES:
continue
filtered_ent_aliases.append(alias_stripped)
all_aliases.extend(filtered_ent_aliases)
try:
from app.core.memory.utils.alias_utils import normalize_aliases
```
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的 Review。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- The special-casing of user placeholder names is duplicated across modules (e.g.,
_USER_PLACEHOLDER_NAMES, hardcoded['用户','我','User','I']in Cypher and write_tools); consider centralizing this list in a shared constant to avoid divergence. - In
write_tools.write,end_user_idis taken fromall_entity_nodes[0]without checking that all nodes share the sameend_user_id; you may want to assert or validate homogeneity to avoid syncing aliases to the wrong user in edge cases. - The PgSQL→Neo4j alias sync only runs when
pg_aliasesis non-empty, which means Neo4j aliases can never be cleared once set; if clearing aliases is a valid scenario, consider explicitly handling the empty-list case instead of skipping.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The special-casing of user placeholder names is duplicated across modules (e.g., `_USER_PLACEHOLDER_NAMES`, hardcoded `['用户','我','User','I']` in Cypher and write_tools); consider centralizing this list in a shared constant to avoid divergence.
- In `write_tools.write`, `end_user_id` is taken from `all_entity_nodes[0]` without checking that all nodes share the same `end_user_id`; you may want to assert or validate homogeneity to avoid syncing aliases to the wrong user in edge cases.
- The PgSQL→Neo4j alias sync only runs when `pg_aliases` is non-empty, which means Neo4j aliases can never be cleared once set; if clearing aliases is a valid scenario, consider explicitly handling the empty-list case instead of skipping.
## Individual Comments
### Comment 1
<location path="api/app/core/memory/agent/utils/write_tools.py" line_range="204-209" />
<code_context>
+ with get_db_context() as db_session:
+ info = EndUserInfoRepository(db_session).get_by_end_user_id(uuid.UUID(end_user_id))
+ pg_aliases = info.aliases if info and info.aliases else []
+ if pg_aliases:
+ await neo4j_connector.execute_query(
+ """
+ MATCH (e:ExtractedEntity)
+ WHERE e.end_user_id = $end_user_id AND e.name IN ['用户', '我', 'User', 'I']
+ SET e.aliases = $aliases
+ """,
+ end_user_id=end_user_id, aliases=pg_aliases,
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider handling the case where PgSQL has an empty alias list by also clearing Neo4j aliases for user entities.
Because the query only runs when `pg_aliases` is non-empty, any intentional clearing of aliases in PgSQL (e.g., user removes data, cleanup job) will not be reflected in Neo4j, leaving stale aliases. To ensure Neo4j reflects PgSQL as the source of truth, consider always executing the query when `info` exists and setting `e.aliases` to an empty list (or `NULL`, per your conventions) when `pg_aliases` is empty.
</issue_to_address>
### Comment 2
<location path="api/app/core/memory/agent/utils/write_tools.py" line_range="207-208" />
<code_context>
+ if pg_aliases:
+ await neo4j_connector.execute_query(
+ """
+ MATCH (e:ExtractedEntity)
+ WHERE e.end_user_id = $end_user_id AND e.name IN ['用户', '我', 'User', 'I']
+ SET e.aliases = $aliases
+ """,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid hardcoding the user placeholder names in Cypher to reduce drift with the Python-side placeholder set.
This `IN ['用户', '我', 'User', 'I']` filter duplicates the placeholder-name set defined in Python (`_USER_PLACEHOLDER_NAMES` / `USER_PLACEHOLDER_NAMES`). If these fall out of sync, some user entities may stop receiving alias updates. Please either pass the Python placeholder list into this query as a parameter or move the list to shared config used by both Python and Cypher.
Suggested implementation:
```python
if pg_aliases:
await neo4j_connector.execute_query(
"""
MATCH (e:ExtractedEntity)
WHERE e.end_user_id = $end_user_id AND e.name IN $user_placeholder_names
SET e.aliases = $aliases
""",
end_user_id=end_user_id,
aliases=pg_aliases,
user_placeholder_names=USER_PLACEHOLDER_NAMES,
)
logger.info(f"[AliasSync] Neo4j 用户实体 aliases 已用 PgSQL 权威源覆盖: {pg_aliases}")
```
1. Ensure that `USER_PLACEHOLDER_NAMES` (or the appropriate constant) is imported or available in this module:
- If defined in this file, make sure it is named `USER_PLACEHOLDER_NAMES`.
- If defined elsewhere (e.g. `app.core.memory.agent.constants`), add an import at the top of this file, such as:
`from app.core.memory.agent.constants import USER_PLACEHOLDER_NAMES`.
2. If the canonical constant is named `_USER_PLACEHOLDER_NAMES` instead, either:
- Import it and alias it to `USER_PLACEHOLDER_NAMES`, or
- Change the parameter value to use the actual name (e.g. `user_placeholder_names=_USER_PLACEHOLDER_NAMES`) and keep the rest the same.
</issue_to_address>
### Comment 3
<location path="api/app/core/memory/storage_services/extraction_engine/deduplication/deduped_and_disamb.py" line_range="91-98" />
<code_context>
- unique_aliases.append(alias_stripped)
+ # 收集所有需要合并的别名
+ all_aliases = list(getattr(canonical, "aliases", []) or [])
+ if incoming_name and incoming_name != canonical_name:
+ all_aliases.append(incoming_name)
+ all_aliases.extend(getattr(ent, "aliases", []) or [])
- # 排序并赋值
- canonical.aliases = sorted(unique_aliases)
+ try:
+ from app.core.memory.utils.alias_utils import normalize_aliases
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider filtering out user placeholder names from incoming aliases to avoid polluting non-user entities.
In this merge path, aliases from `ent` (including `incoming_name`) are added to `canonical` whenever `canonical_name` is not in `_USER_PLACEHOLDER_NAMES`. If an entity with a user placeholder name (e.g., “我”/“User”) is merged into a non-user canonical entity, that placeholder will be stored as an alias, which conflicts with the intent that user aliases are handled only via PgSQL. To prevent this, skip adding aliases that are in `_USER_PLACEHOLDER_NAMES` (case-insensitive) for both `incoming_name` and `ent.aliases`.
```suggestion
# 收集所有需要合并的别名
all_aliases = list(getattr(canonical, "aliases", []) or [])
# 过滤掉用户占位名,避免污染非用户实体的别名集合(对 incoming_name)
if incoming_name and incoming_name != canonical_name:
incoming_name_stripped = incoming_name.strip()
if incoming_name_stripped and incoming_name_stripped.lower() not in _USER_PLACEHOLDER_NAMES:
all_aliases.append(incoming_name_stripped)
# 过滤掉用户占位名,避免污染非用户实体的别名集合(对 ent.aliases)
ent_aliases = getattr(ent, "aliases", []) or []
filtered_ent_aliases = []
for alias in ent_aliases:
if not isinstance(alias, str):
continue
alias_stripped = alias.strip()
if not alias_stripped:
continue
if alias_stripped.lower() in _USER_PLACEHOLDER_NAMES:
continue
filtered_ent_aliases.append(alias_stripped)
all_aliases.extend(filtered_ent_aliases)
try:
from app.core.memory.utils.alias_utils import normalize_aliases
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
api/app/core/memory/storage_services/extraction_engine/deduplication/deduped_and_disamb.py
Outdated
Show resolved
Hide resolved
…logic - Replace hardcoded user placeholder name lists in write_tools and user_memory_service with shared _USER_PLACEHOLDER_NAMES constant - Filter user placeholder names during alias merging in _merge_attribute to prevent cross-role alias contamination on non-user entities - Use toLower() in Cypher query for case-insensitive name matching - Change PgSQL->Neo4j alias sync condition from 'if pg_aliases' to 'if info is not None' so empty aliases correctly clear stale data
keeees
approved these changes
Apr 14, 2026
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.
…iases
Summary by Sourcery
使 PostgreSQL 成为用户别名(alias)的唯一可信来源,并确保 Neo4j 和分析逻辑遵循该模型。
Bug Fixes:
ExtractedEntity标签和entity_type属性。Enhancements:
MERGECypher 增加保护,使写操作不会修改 Neo4j 中用户占位符实体的别名。Original summary in English
Summary by Sourcery
Make PostgreSQL the single source of truth for user aliases and ensure Neo4j and analytics respect that model.
Bug Fixes:
Enhancements: