Sisense Community logo
    • Community Feedback
    • Chapters
    • Events
    • Forums
      • Help and How To
      • Product Feedback Forum
      • Strategy & Use Cases
    • Blogs
    • KB Docs
      • KB Docs
      • Add-Ons & Plug-Ins
      • APIs
      • Best Practices
      • Blox
      • CDT
      • Cloud Managed Service
      • Data Models
      • Data Sources
      • Embedding Analytics
      • How-Tos & FAQs
      • Onboarding
      • PySisense
      • Security
      • Sisense Administration
      • Sisense Intelligence & AI
      • Troubleshooting
      • Widget & Dashboard Scripts
    • Support
    • Learning
      • Sisense Academy: Free Courses and Certifications
      • Official Developer Documentation
      • Official Product Documentation
      • Official Sisense Youtube Channel
      • Sisense Compose SDK Playground
      • Official Sisense Discord
    • Use Case Gallery
    Discussions
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    Discussions
    • TagsChevronRightIcon
    Troubleshooting: Linux
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Setting Maximum Panel Items on Widget Panels via Scripting

                                                                                       

      Setting Maximum Items on Widget Panels via Script Sisense widgets set max panel item per type (values, rows, etc) limits that control how many dimensions, measures, and filters can be added to each panel. When these limits are reached, the Add Panel button is hidden, preventing users from adding additional fields. A widget script that sets the maxitems property on each panel is commonly used to raise (or occasionally lower) this limit. This very commonly used script may be experiencing issues on some new releases of Sisense (L2026.2.2-c). Dashboard designers and developers who have this script in place and are not experiencing any issues may leave the script unchanged. Dashboard designers who are experiencing problems can replace the script with the version shared below. For use cases that specifically use this script for pivot widgets and want it applied automatically across many widgets or configured per user group, the PivotMaxPanelItems plugin is a more scalable alternative. See Extending Pivot Widget Panel Limits in Sisense Using User Groups for details. Script That May Not Work The following script is commonly used as a widget script to raise the panel item limit: widget.manifest.data.panels.forEach(function(p) { p.metadata.maxitems = 40; // replace with the value needed }); This script accesses the panel metadata maxitems parameter directly through widget.manifest.data.panels . Based on current behavior, this path appears to be broken in some environments on specific Sisense versions.

      Without the script applied, if a panel type includes too many items, the Add Panel Button is hidden. Replacement Script The following script produces the same result and can be more reliable. widget.on('initialized', function() { widget.metadata.panels.forEach(function(p) { p.$$manifest.metadata.maxitems = 60; // replace with the value needed }); }); This version waits for the initialized event before accessing panel data, and references the panel's $$manifest.metadata path rather than going through widget.manifest.data . Dashboard designers and developers should update the maxitems value to match the limit needed for their use case. Note that setting a very high limit and adding a large number of fields to a widget may result in extended load times.
      With the script applied, the Add Panel Button does not disappear when below the new limit, and as many dimensions and fields as needed can be added to the widget. Applying This to More Complex Scripts and Plugins This potential fix applies to any plugin or script that sets maxitems on widget panels and uses this exact path. If a plugin or script accesses panel metadata through widget.manifest.data.panels and is functioning correctly, no changes are necessary. If it appears to no longer work, updating it to use widget.metadata.panels and reference p.$$manifest.metadata is likely to fix the issue. The scripts above are simply examples. More dynamic implementations, such as those that read current values, apply conditional logic, or use different values based on user type or user group, follow the same principle, if the older method is not working, replacing the maxitems path is a likely fix.

      Jeremy Friedel
      Jeremy FriedelPosted 3 weeks ago
      0
               
    • Ihor Yokhym
      • Knowledge Base Docs
               
      Ihor Yokhym
      Fixing Snowflake connection failure caused by JDBC driver NullPointerException [Linux]
               

      Introduction Resolve the Snowflake JDBC driver "HTTP request: null" communication error in Sisense (Cloud and On-Premises). This guide outlines how to upgrade the Snowflake JDBC driver to version 3.24.2 or later to bypass a known NullPointerException bug. Step-by-Step Guide Symptom When attempting to connect to Snowflake, the connection fails, and the logs display the following error message: Code Type: Error Message JDBC driver encountered communication error. Message: Exception encountered for HTTP request: null. This behavior stems from a known issue within the Snowflake JDBC driver itself (SNOW-1958601: NullPointerException for cacheFile in net.snowflake:snowflake-jdbc:3.23.0 ). The issue is resolved in connector version 3.23.2 and later or upgrade to Sisense version 2026.2.1+ . To completely safeguard your environment, follow the steps below to upgrade to version 3.24.2. Step 1: Download the Updated JDBC Driver Download the stable snowflake-jdbc-3.24.2.jar file or later directly from the official Snowflake repository or Maven Central. Maven repository: download driver Snowflake documentation Step 2: Upload the Driver to File Management Navigate to the File Management section on your Sisense server and upload the newly downloaded .jar file to the target Snowflake connector directory. /connectors/framework/SnowflakeJDBC/ [Screenshot: Sisense File Management interface showing the SnowflakeJDBC directory] Description: The image shows the Sisense File Management browser with the snowflake-jdbc-3.24.2.jar file successfully uploaded into the connectors framework directory. Step 3: Update the Connector Configuration Open the Sisense Admin portal. Navigate to Service Configuration → Manage Connectors → SnowflakeJDBC. Update the configuration properties to point directly to the new driver version ( 3.24.2 ). [Screenshot: Service Configuration UI for Manage Connectors] Description: This screenshot highlights the driver version field within the SnowflakeJDBC configuration menu where the version number needs to be updated. Step 4: Restart the Query Service To apply the changes and force Sisense to initialize the new JDBC driver, restart the Query service. Conclusion A known NullPointerException bug in the 3.23.0 Snowflake JDBC driver causes connection handshakes to fail with a generic null HTTP request error. Upgrading your Sisense environment to use the 3.24.2 driver patch (or later) via Service Configuration and executing a Query service restart successfully resolves the communication roadblock. References/Related Content  Maven repository: download driver Snowflake documentation GitHub Issue #2105: snowflakedb/snowflake-jdbc NullPointerException

      1 month ago
      0
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Managing Sisense Plugin States via the REST API

                                                                               

      Managing Sisense Plugin States via the REST API Overview Manually enabling and disabling Sisense plugins through the admin interface can become time consuming, particularly when keeping multiple Sisense server instances in a consistent state, switching active plugin lists quickly for testing, restoring a previous known working plugin list after an upgrade or support session, or iterating on a plugin during development where having only one plugin active significantly reduces build time for any plugin changes to become active. This article describes a Python command line (CLI) tool that manages Sisense plugin states via the Sisense REST API, using a simple configuration file as the config. The tool supports enabling and disabling plugins, isolating (enabling only one and disabling all others) a single plugin for faster development builds, saving and restoring named snapshots of plugin state, and previewing changes before applying them. This tool does not install or uninstall plugins, and does not update plugins or upgrade/downgrade or modify the code within the plugins. The script, configuration file, and dependencies are included in this article. Sisense instances often have many plugins active, with the enabled list changing over time. Common scenarios include: Plugins being left enabled or disabled after support sessions or testing, or enabling or disabling all plugins The need to quickly reach a server plugin state with no plugins enabled, to isolate whether a plugin is causing a potential issue Maintaining a consistent plugin list across multiple Sisense environments or after upgrades Needing to roll back to a previous plugins active state after a change is potentially creating issues (This tool does not rollback plugin versions, only active plugin lists) Performing these operations manually through the Sisense UI admin panel is practical for occasional changes, but becomes inefficient when the changes are frequent, when many plugins are involved, or when the same configuration needs to be applied reliably and repeatably. Description The plugin manager script reads an enable_plugins list from a configuration file and syncs the Sisense instance to match via the REST API. The script: Enables plugins listed in enable_plugins in the configuration file Optionally disables plugins listed in disable_plugins Leaves all other plugins in their current state by default Saves a timestamped snapshot of the current plugin state before applying any changes, providing an automatic rollback point Supports restoring any saved snapshot to return to a previous state A strict mode is also available, which disables any currently enabled plugin that is not listed in enable_plugins , making the instance exactly match the configuration file. The script also supports previewing what would change without making any API calls, and can save named snapshots for later reference or comparison. Plugin name matching Plugin names in the configuration file accept either the short form ( SwitchDimension ) or the full form ( plugin-SwitchDimension ). Matching is case-insensitive. This applies both to the configuration file and to command line arguments. Prerequisites Python 3.7 or later Network access to the Sisense instance (The tool runs remotely and does not need to be installed directly on the server, or SSH access) A Sisense API token, which can be generated from Admin > REST API > v1.0 > GET /authentication/tokens/api Setup Install the required Python packages: pip install requests pyyaml Create a config.yaml and fill in the Sisense instance URL and API token: sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE Add the plugins to enable to the enable_plugins list. See the Configuration reference section for all available settings. The script is then ready to run. Configuration reference sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE # Admin → REST API → v1.0 → GET /authentication/tokens/api bulk: true # set false to update plugins one at a time instead of a single batch request # Plugins to enable. # Accepts short names (SwitchDimension) or full names (plugin-SwitchDimension). # Matching is case-insensitive. # # Default behavior: enable listed plugins; leave every other plugin unchanged. # With --strict: also disable any currently-enabled plugin NOT in this list, # making the instance exactly match this config. enable_plugins: - examplePlugin # Plugins to explicitly disable (optional). # Only needed in the default sync (no --strict), where unlisted plugins are # left alone. Listing a plugin here ensures it stays off and documents the # intentional exclusion. # disable_plugins: # - anotherExamplePlugin enable_plugins : the list of plugins to enable. By default, plugins not on this list are left in their current state. With --strict , any currently enabled plugin not on this list will be disabled. disable_plugins : optional. Plugins to explicitly disable in the default sync without --strict . This is useful for documenting intentional exclusions when strict mode is not being used. bulk : controls whether plugin state changes are sent as a single batch API request or one at a time. Set to false for individual requests, which is slower but can be useful for diagnosing issues with specific plugins. Usage Run the script with no arguments to enable plugins in enable_plugins , disable plugins in disable_plugins , and leave everything else unchanged: python plugin_manager.py Before making any changes, the script saves a timestamped snapshot of the current plugin state. To restore the most recent snapshot: python plugin_manager.py --restore-snapshot To preview what would change without making any API calls: python plugin_manager.py --dry-run Common Use Cases Maintaining a known good plugin baseline Organizations that need to ensure consistent plugin configuration across upgrades, support sessions, or environment changes can maintain an enable_plugins list in the configuration file. Running the script applies the defined configuration to the instance. To also disable any currently enabled plugin not in the list: python plugin_manager.py --strict Testing on a stock Sisense instance Plugin interference is a common source of issues that are hard to reproduce. To quickly disable all plugins and test against a clean instance, clear enable_plugins in the configuration file and run with --strict . The current state is saved automatically before any changes are made, and can be restored when testing is complete: # with enable_plugins cleared in config.yaml python plugin_manager.py --strict # restore when done python plugin_manager.py --restore-snapshot Developing and testing a single plugin When many plugins are installed, Sisense rebuilds all of them whenever any plugin changes. Using --enable-one disables all currently enabled plugins and enables only the specified plugin, which can significantly reduce rebuild time on instances with many plugins: python plugin_manager.py --enable-one MyPlugin Snapshots The script saves snapshots as YAML files to a snapshots/ folder alongside the script. Snapshots capture the full enabled and disabled plugin state at a point in time. Snapshots are saved automatically before any run that changes state. Named snapshots can also be saved and restored explicitly: # save the current state with a specific name python plugin_manager.py --save-snapshot before-upgrade # list all saved snapshots python plugin_manager.py --list-snapshots # restore a specific snapshot python plugin_manager.py --restore-snapshot before-upgrade Including --log-version when saving a snapshot records the version of each plugin at that point in time, which can be useful for tracking plugin versions across upgrades. Full option reference usage: plugin_manager.py [-h] [--config FILE] [--dry-run] [--no-bulk] [--skip-snapshot] [--log-version] [--strict] [--enable-only | --disable-only] [--enable-one PLUGIN | --save-snapshot [NAME] | --list-snapshots | --restore-snapshot [NAME]] Manage Sisense plugin states via the REST API. options: -h, --help show this help message and exit --config FILE Path to config.yaml (default: config.yaml next to this script) --dry-run Preview what would change; make no API calls and save no snapshot --no-bulk Send one API call per plugin instead of a single batch PATCH. Slower but easier to diagnose partial failures. Overrides sisense.bulk in config. --skip-snapshot Do not automatically save a snapshot before this run --log-version Record each plugin's version field (from the API response) in any snapshot saved during this run (manual --save-snapshot or auto saved snapshot) --strict Also disable every currently enabled plugin that is not listed in enable_plugins. Without this flag, only plugins in disable_plugins are disabled; everything else is left alone. Always active for --restore- snapshot. --enable-only Run only the enable step; skip all disables --disable-only Run only the disable step; skip all enables --enable-one PLUGIN Enable exactly one plugin by name and disable all currently enabled others. Accepts the short or full plugin name. --save-snapshot [NAME] Save the current plugin state as a named snapshot. Omit NAME for an auto generated timestamp filename. Combine with --log-version to also record versions for each plugin. --list-snapshots List all saved snapshots with creation date and enabled plugin count --restore-snapshot [NAME] Restore plugin state to exactly match a saved snapshot. Omit NAME to use the most recently created snapshot. Always strict (disables plugins not in snapshot) unless --enable-only. Plugin names accept the short form (SwitchDimension) or the full form (plugin-SwitchDimension) in both CLI args and config.yaml. Default behavior: enable plugins in enable_plugins, disable plugins in disable_plugins, leave everything else unchanged. Use --strict to also disable any enabled plugin not in enable_plugins. A snapshot is saved automatically before each run that modifies state. Use --skip-snapshot to suppress this. Use Case Managing plugins via the REST API and a configuration file offers several advantages over manual administration: Applies plugin active list consistently and repeatably without manual steps Provides automatic rollback through snapshots saved before each change Supports quickly switching between plugin states for testing or development Reduces the time required to restore a known working plugin list after an issue is resolved Gives server administrators a record of plugin state changes over time (done via the tool and saved via snapshots) Summary With this tool in place, administrators and developers can manage Sisense plugin states quickly and reliably from the command line. The configuration file serves as a single source of truth for the intended plugin set, and the snapshot system ensures that any change can be reversed. Whether maintaining a consistent production environment, isolating issues related to plugins, or iterating on plugin development, the tool provides a practical and repeatable approach to plugin state management. The code is not compressed or obfuscated and can be modified as needed or used as a reference for similar tools. Full Python Code: """ Sisense Plugin Manager: manage Sisense plugin states via the REST API. Overview Manages Sisense plugin states via the REST API. Treats plugin configuration as code: define which plugins should be enabled in config.yaml, run the script, and the instance matches, quickly, from anywhere with network access. Before applying any changes, the current plugin state is automatically captured in a timestamped snapshot. If a change needs to be reverted, a single --restore-snapshot command rolls the instance back to exactly how it was. By default only the plugins explicitly listed in config.yaml are enabled or disabled, everything else is left as is. Use --strict to also disable any enabled plugin not in the list, making the instance enabled plugin list exactly match the config. Common use cases Maintain a known good plugin baseline Keep enable_plugins in config.yaml up to date. Run the script after upgrades, support sessions, or any time the instance may have drifted from the expected state. Test against a stock (no plugins) Sisense instance Eliminate plugins as a potential cause of a bug or issue. Clear enable_plugins in config.yaml, run --strict to disable everything, test, then restore when done, the snapshot is saved automatically before changes: # (clear enable_plugins in config.yaml) python plugin_manager.py --strict # ... reproduce the issue on a vanilla instance ... python plugin_manager.py --restore-snapshot # restores most recent Develop and iterate on a single plugin quickly With many plugins installed Sisense rebuilds all of them on each change. --enable-one isolates a single plugin and disables the rest, cutting rebuild time to a fraction on instances with many plugins: python plugin_manager.py --enable-one MyPlugin Roll back any change instantly Every run saves a timestamped snapshot of the state it found before making changes. Use --list-snapshots to find an earlier state and --restore-snapshot to return to it. Usage # Enable listed plugins, disable listed disable_plugins, leave rest unchanged python plugin_manager.py # Strict: also disables any enabled plugin not in enable_plugins python plugin_manager.py --strict # Preview what would change (no API calls, no auto snapshot) python plugin_manager.py --dry-run python plugin_manager.py --strict --dry-run # Run only one half of the sync python plugin_manager.py --enable-only # enable listed; skip all disables python plugin_manager.py --disable-only # disable listed; skip all enables # Enable exactly one plugin and disable every other currently enabled plugin python plugin_manager.py --enable-one SwitchDimension # Snapshots python plugin_manager.py --save-snapshot [NAME] # name is optional python plugin_manager.py --save-snapshot [NAME] --log-version # also record per-plugin versions python plugin_manager.py --list-snapshots python plugin_manager.py --restore-snapshot [NAME] # omit NAME → most recent # Other flags (combinable with any of the above) --no-bulk one API call per plugin instead of a single batch request --skip-snapshot do not save an auto-snapshot before this run --log-version record each plugin's version in any snapshot saved this run --config FILE use a config file other than the default config.yaml Plugin names All plugin names accept either the short form (SwitchDimension) or the full form (plugin-SwitchDimension), both in CLI arguments and in config.yaml. Matching is case insensitive. API token Admin → REST API → v1.0 → GET /authentication/tokens/api Setup pip install requests pyyaml # Python 3.7+ required cp config.yaml.example config.yaml # Edit config.yaml: set sisense.url, sisense.token, and enable_plugins list """ import sys from datetime import datetime, timezone from pathlib import Path from typing import Optional import requests import yaml # Constants SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG_PATH = SCRIPT_DIR / "config.yaml" SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots" PLUGIN_PAGE_SIZE = 20 # plugins returned per page by GET /api/v1/plugins # Config helpers def load_config(config_path: Path) -> dict: """Load and return the YAML config file, printing an error and exiting if missing.""" if not config_path.exists(): print(f"Config file not found: {config_path}") print("Copy config.yaml.example to config.yaml and fill in your instance URL and API token.") sys.exit(1) with config_path.open() as file_handle: return yaml.safe_load(file_handle) def make_auth_headers(api_token: str) -> dict: """Return the Authorization + Content-Type headers required by the Sisense REST API.""" return { "Authorization": f"Bearer {api_token}", "Content-Type": "application/json", } # Plugin name normalization def normalize_plugin_name(raw_name: str) -> str: """ Return a canonical form: lowercase with any leading 'plugin-' prefix stripped. Both 'plugin-SwitchDimension' and 'SwitchDimension' normalize to 'switchdimension', so callers do not need to be consistent about the prefix. """ lowered = raw_name.lower() return lowered[len("plugin-"):] if lowered.startswith("plugin-") else lowered def build_name_lookup(plugin_names: list) -> set: """ Build a set that matches a plugin regardless of whether the caller used the 'plugin-' prefix. Each name contributes two entries: the bare normalized form and the 'plugin-<normalized>' form. Use with plugin_matches_lookup() to test whether an API plugin object is covered by a configured list. """ lookup: set = set() for name in plugin_names: normalized = normalize_plugin_name(name) lookup.add(normalized) lookup.add("plugin-" + normalized) return lookup def plugin_matches_lookup(plugin: dict, name_lookup: set) -> bool: """ Return True if either the plugin's folderName or its API name appears in name_lookup (case insensitive, prefix agnostic). """ plugin_identifiers = {plugin["folderName"].lower(), plugin["name"].lower()} return bool(plugin_identifiers & name_lookup) # Plugin list API def fetch_all_plugins(base_url: str, auth_headers: dict) -> list: """ Retrieve the full plugin list from the Sisense instance via paginated GET requests. Prints progress to stdout and exits on HTTP error. """ print("Fetching plugin list...") try: all_plugins = _paginate_plugins(base_url, auth_headers) except requests.HTTPError as http_error: print(f"Failed to fetch plugins: {http_error}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") return all_plugins def _paginate_plugins(base_url: str, auth_headers: dict) -> list: """Internal: collect every page from GET /api/v1/plugins.""" endpoint = f"{base_url}/api/v1/plugins" all_plugins: list = [] offset = 0 while True: response = requests.get( endpoint, headers=auth_headers, params={"limit": PLUGIN_PAGE_SIZE, "skip": offset}, timeout=30, ) response.raise_for_status() payload = response.json() page = payload.get("plugins", []) all_plugins.extend(page) offset += len(page) total_count = payload.get("count", 0) print(f" Fetched {offset} / {total_count} plugins...") if offset >= total_count or not page: break return all_plugins # Plugin versions def extract_plugin_versions(plugins: list) -> dict: """ Extract the version field from each plugin object in an API response. Returns a dict mapping folderName → version string for every plugin that carries a version field. Plugins with no version field are omitted so the caller can treat an absent key as 'version unknown'. """ versions: dict = {} for plugin in plugins: version_value = plugin.get("version") or plugin.get("pluginVersion") if version_value: versions[plugin["folderName"]] = str(version_value) return versions # Applying changes def _patch_plugins(base_url: str, auth_headers: dict, updates: list) -> None: """Send a PATCH /api/v1/plugins request. Raises requests.HTTPError on failure.""" response = requests.patch( f"{base_url}/api/v1/plugins", headers=auth_headers, json=updates, timeout=60, ) response.raise_for_status() def apply_plugin_updates( base_url: str, auth_headers: dict, updates: list, use_bulk: bool, ) -> tuple: """ Apply a list of enable/disable changes and print the outcome per plugin. use_bulk=True → single PATCH with all updates at once (faster, one round-trip). use_bulk=False → one PATCH per plugin (slower, easier to diagnose partial failures). Returns (success_count, error_count). """ if use_bulk: print(f"Sending bulk request ({len(updates)} plugin(s))...") try: _patch_plugins(base_url, auth_headers, updates) except requests.HTTPError as http_error: print(f" [ERROR] Bulk request failed: {http_error}") sys.exit(1) for update in updates: status_label = "ENABLED" if update["isEnabled"] else "DISABLED" print(f" [{status_label}] {update['folderName']}") return len(updates), 0 success_count = error_count = 0 for update in updates: folder_name = update["folderName"] status_label = "ENABLED" if update["isEnabled"] else "DISABLED" try: _patch_plugins(base_url, auth_headers, [update]) print(f" [{status_label}] {folder_name}") success_count += 1 except requests.HTTPError as http_error: print(f" [ERROR] {folder_name}: {http_error}") error_count += 1 return success_count, error_count # Snapshot I/O def save_snapshot( snapshot_name: str, enabled_folders: list, *, disabled_folders: Optional[list] = None, plugin_versions: Optional[dict] = None, ) -> Path: """ Write a snapshot YAML file to snapshots/<snapshot_name>.yaml. Fields written (in this order): enable_plugins: sorted list of currently enabled folderNames disabled_plugins: sorted list of currently-disabled folderNames (when provided) plugin_versions: {folderName: version} for each plugin (when provided) created: ISO-8601 UTC timestamp Key ordering is preserved (sort_keys=False) so the file reads naturally. """ SNAPSHOTS_DIR.mkdir(exist_ok=True) # created goes last so the plugin lists appear first in the file snapshot_data: dict = {"enable_plugins": sorted(enabled_folders)} if disabled_folders is not None: snapshot_data["disabled_plugins"] = sorted(disabled_folders) if plugin_versions: snapshot_data["plugin_versions"] = plugin_versions snapshot_data["created"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") snapshot_path = SNAPSHOTS_DIR / f"{snapshot_name}.yaml" with snapshot_path.open("w") as file_handle: yaml.dump(snapshot_data, file_handle, sort_keys=False, default_flow_style=False, allow_unicode=True) return snapshot_path def _snapshot_enable_list(snapshot: dict) -> list: """ Return the enabled plugin list from a snapshot, handling both the current 'enable_plugins' key and the legacy 'plugins' key used by older snapshots. """ return snapshot.get("enable_plugins") or snapshot.get("plugins", []) def load_snapshot(snapshot_name: str) -> dict: """Load a snapshot by stem name, exiting with an error if the file is missing.""" snapshot_path = SNAPSHOTS_DIR / f"{snapshot_name}.yaml" if not snapshot_path.exists(): print(f"Snapshot not found: {snapshot_path}") sys.exit(1) with snapshot_path.open() as file_handle: return yaml.safe_load(file_handle) def list_snapshot_files() -> list: """Return all snapshot YAML files in the snapshots directory, sorted by filename.""" if not SNAPSHOTS_DIR.exists(): return [] return sorted(SNAPSHOTS_DIR.glob("*.yaml")) def latest_snapshot_name() -> str: """ Return the stem of the snapshot with the most recent 'created' timestamp. Exits if no snapshots exist. """ snapshot_files = list_snapshot_files() if not snapshot_files: print("No snapshots found.") sys.exit(1) def created_timestamp(path: Path) -> str: with path.open() as file_handle: return yaml.safe_load(file_handle).get("created", "") return max(snapshot_files, key=created_timestamp).stem def auto_save_snapshot(plugins: list, include_version: bool) -> None: """ Save a snapshot from an already fetched plugin list, before applying changes. Accepts the plugin list fetched at the start of the run so no second API call is needed. Called before any changes are applied, giving every run that changes state a rollback point. Failures are caught and reported as a warning so a snapshot error never blocks the intended change. """ snapshot_name = datetime.now().strftime("auto_%Y%m%d_%H%M%S") print(f"\nSaving snapshot '{snapshot_name}' before applying changes...") try: enabled_folders = [p["folderName"] for p in plugins if p.get("isEnabled")] disabled_folders = [p["folderName"] for p in plugins if not p.get("isEnabled")] plugin_versions = extract_plugin_versions(plugins) if include_version else None snapshot_path = save_snapshot( snapshot_name, enabled_folders, disabled_folders=disabled_folders or None, plugin_versions=plugin_versions, ) print(f" Saved to {snapshot_path} ({len(enabled_folders)} enabled / {len(disabled_folders)} disabled)") except Exception as exc: print(f" Warning: auto snapshot failed: {exc}") # Sync computation def compute_plugin_updates( all_plugins: list, enable_names: list, disable_names: list, *, strict: bool, enable_only: bool, disable_only: bool, ) -> tuple: """ Determine which plugins need their state changed. Default behavior (strict=False): - Enable: plugins in enable_names that are currently disabled. - Disable: plugins in disable_names that are currently enabled. - Leave alone: everything else (not mentioned in either list). With strict=True: - Enable: same as default. - Disable: plugins in disable_names, PLUS any currently enabled plugin that does not appear in enable_names. This makes the instance exactly match enable_names. Direction overrides (applied after the above): enable_only=True → clears to_disable entirely. disable_only=True → clears to_enable entirely. Returns: to_enable: sorted folderNames to enable to_disable: sorted folderNames to disable unmatched_names: entries from enable_names that matched no instance plugin """ enable_lookup = build_name_lookup(enable_names) disable_lookup = build_name_lookup(disable_names) # Plugins from the instance that match the enable list matched_enable_plugins = [p for p in all_plugins if plugin_matches_lookup(p, enable_lookup)] enable_folder_set = {plugin["folderName"] for plugin in matched_enable_plugins} # Plugins that should be enabled but currently are not to_enable: list = [] if disable_only else sorted( plugin["folderName"] for plugin in matched_enable_plugins if not plugin.get("isEnabled") ) # Plugins that should be disabled but currently are enabled if enable_only: to_disable: list = [] elif strict: # Strict: disable every enabled plugin not in the enable list to_disable = sorted( plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled") and plugin["folderName"] not in enable_folder_set ) else: # Soft: only disable what is explicitly listed in disable_plugins to_disable = sorted( plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled") and plugin_matches_lookup(plugin, disable_lookup) ) # Entries from enable_names that found no matching plugin in the instance matched_normalized_names = ( {normalize_plugin_name(plugin["folderName"]) for plugin in matched_enable_plugins} | {normalize_plugin_name(plugin["name"]) for plugin in matched_enable_plugins} ) unmatched_names = sorted( name for name in enable_names if normalize_plugin_name(name) not in matched_normalized_names ) return to_enable, to_disable, unmatched_names def _build_update_payload(to_enable: list, to_disable: list) -> list: """Combine enable and disable lists into the PATCH payload format.""" return ( [{"folderName": folder_name, "isEnabled": True} for folder_name in to_enable] + [{"folderName": folder_name, "isEnabled": False} for folder_name in to_disable] ) def _execute_sync( base_url: str, auth_headers: dict, to_enable: list, to_disable: list, use_bulk: bool, ) -> int: """ Build the PATCH payload from to_enable / to_disable and apply it. Prints 'Nothing to change.' if both lists are empty. Returns the error count (0 on full success). """ updates = _build_update_payload(to_enable, to_disable) if not updates: print("Nothing to change.") return 0 _, error_count = apply_plugin_updates(base_url, auth_headers, updates, use_bulk) return error_count # Output helpers def _print_update_plan( to_enable: list, to_disable: list, already_correct_count: int, untouched_count: int = 0, ) -> None: """Print a summary of pending changes before they are applied.""" print(f" To enable: {len(to_enable)}") print(f" To disable: {len(to_disable)}") if already_correct_count > 0: print(f" Already set: {already_correct_count}") if untouched_count > 0: print(f" Untouched: {untouched_count} (not listed in config; left as-is)") print() def _print_dry_run_preview(to_enable: list, to_disable: list) -> None: """Print the would-be changes when --dry-run is active.""" if not to_enable and not to_disable: print("Nothing to change.") if to_enable: print("Would enable:") for folder_name in to_enable: print(f" + {folder_name}") if to_disable: print("Would disable:") for folder_name in to_disable: print(f" - {folder_name}") print("\n[DRY RUN] No changes made.") def _report_unmatched_names(unmatched_names: list) -> None: """Warn about configured plugin names that matched nothing in the instance.""" if unmatched_names: print(f"\nNot found in instance ({len(unmatched_names)}):") for name in unmatched_names: print(f" - {name}") # Commands def cmd_sync_to_config( base_url: str, auth_headers: dict, enable_names: list, disable_names: list, *, dry_run: bool, use_bulk: bool, strict: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: """ Default command: sync the instance to match config.yaml. Enables plugins in enable_plugins; in strict mode, also disables any currently enabled plugin not in enable_plugins. Without strict, only plugins explicitly in disable_plugins are disabled. """ all_plugins = fetch_all_plugins(base_url, auth_headers) to_enable, to_disable, unmatched_names = compute_plugin_updates( all_plugins, enable_names, disable_names, strict=strict, enable_only=enable_only, disable_only=disable_only, ) enable_lookup = build_name_lookup(enable_names) disable_lookup = build_name_lookup(disable_names) matched_plugins = [p for p in all_plugins if plugin_matches_lookup(p, enable_lookup)] already_correctly_enabled = sum(1 for p in matched_plugins if p.get("isEnabled")) # Untouched: plugins in neither list, only relevant without --strict, where # we deliberately leave them alone untouched_count = 0 if strict or enable_only else sum( 1 for p in all_plugins if not plugin_matches_lookup(p, enable_lookup) and not plugin_matches_lookup(p, disable_lookup) ) # Table of plugins matched by the enable list if matched_plugins: print(f"Matched {len(matched_plugins)} plugin(s) from enable_plugins:\n") print(f" {'Folder name':<45} {'API name':<35} {'Enabled'}") print(f" {'-'*45} {'-'*35} {'-'*7}") for plugin in sorted(matched_plugins, key=lambda p: p["folderName"]): print(f" {plugin['folderName']:<45} {plugin['name']:<35} {plugin['isEnabled']}") print() _print_update_plan(to_enable, to_disable, already_correctly_enabled, untouched_count) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched_names(unmatched_names) return if not skip_snapshot and (to_enable or to_disable): auto_save_snapshot(all_plugins, include_version) error_count = _execute_sync(base_url, auth_headers, to_enable, to_disable, use_bulk) print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {error_count}") _report_unmatched_names(unmatched_names) def cmd_enable_single_plugin( base_url: str, auth_headers: dict, plugin_name: str, *, dry_run: bool, use_bulk: bool, skip_snapshot: bool, include_version: bool, ) -> None: """ Enable exactly one plugin and disable every other currently enabled plugin. The target plugin is identified by name (short or full form, case insensitive). This command does not consult enable_plugins or disable_plugins in config. """ target_name_lookup = build_name_lookup([plugin_name]) all_plugins = fetch_all_plugins(base_url, auth_headers) target_plugin = next( (plugin for plugin in all_plugins if plugin_matches_lookup(plugin, target_name_lookup)), None, ) if target_plugin is None: print(f"Plugin not found: {plugin_name!r}") print("Tip: check spelling against the Sisense admin panel, or run --save-snapshot to capture current names.") sys.exit(1) target_folder = target_plugin["folderName"] target_currently_on = target_plugin.get("isEnabled", False) to_enable = [] if target_currently_on else [target_folder] to_disable = sorted( plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled") and plugin["folderName"] != target_folder ) print(f"Target plugin: {target_folder}") print(f" Currently: {'enabled' if target_currently_on else 'disabled'}\n") _print_update_plan( to_enable, to_disable, already_correct_count=1 if target_currently_on else 0, ) if dry_run: _print_dry_run_preview(to_enable, to_disable) return if not skip_snapshot and (to_enable or to_disable): auto_save_snapshot(all_plugins, include_version) error_count = _execute_sync(base_url, auth_headers, to_enable, to_disable, use_bulk) print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {error_count}") def cmd_save_named_snapshot( base_url: str, auth_headers: dict, snapshot_name: str, include_version: bool, ) -> None: """Save the current plugin state as a named snapshot file.""" all_plugins = fetch_all_plugins(base_url, auth_headers) enabled_folders = [plugin["folderName"] for plugin in all_plugins if plugin.get("isEnabled")] disabled_folders = [plugin["folderName"] for plugin in all_plugins if not plugin.get("isEnabled")] plugin_versions = extract_plugin_versions(all_plugins) if include_version else None snapshot_path = save_snapshot( snapshot_name, enabled_folders, disabled_folders=disabled_folders or None, plugin_versions=plugin_versions, ) print(f"\nSnapshot '{snapshot_name}' saved to {snapshot_path}") print(f" {len(enabled_folders)} enabled / {len(disabled_folders)} disabled plugin(s) captured.") if plugin_versions is not None: print(f" Versions recorded for {len(plugin_versions)} plugin(s).") def cmd_list_snapshots() -> None: """Print a formatted table of all saved snapshots.""" snapshot_files = list_snapshot_files() if not snapshot_files: print("No snapshots found.") return print(f"Snapshots ({len(snapshot_files)}):\n") print(f" {'Name':<32} {'Created':<22} {'Enabled':<9} {'Versions'}") print(f" {'-'*32} {'-'*22} {'-'*9} {'-'*8}") for snapshot_path in snapshot_files: with snapshot_path.open() as file_handle: snapshot_data = yaml.safe_load(file_handle) enabled_count = len(_snapshot_enable_list(snapshot_data)) versions_logged = "yes" if snapshot_data.get("plugin_versions") else "no" print( f" {snapshot_path.stem:<32} " f"{snapshot_data.get('created', 'unknown'):<22} " f"{enabled_count:<9} " f"{versions_logged}" ) def cmd_restore_from_snapshot( base_url: str, auth_headers: dict, snapshot_name: str, *, dry_run: bool, use_bulk: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: """ Restore plugin state to exactly match a saved snapshot. Restore is always strict: plugins in the snapshot are enabled and all other currently enabled plugins are disabled, matching the state at snapshot time. Use --enable-only to skip the disable step (only enable what the snapshot lists). """ snapshot = load_snapshot(snapshot_name) snapshot_enable_list = _snapshot_enable_list(snapshot) print(f"Snapshot '{snapshot_name}' ({snapshot.get('created', 'unknown')})") if snapshot.get("disabled_plugins"): print(f" {len(snapshot_enable_list)} enabled / {len(snapshot['disabled_plugins'])} disabled at snapshot time") else: print(f" {len(snapshot_enable_list)} plugin(s) enabled in snapshot") if snapshot.get("plugin_versions"): print(f" Plugin versions recorded: {len(snapshot['plugin_versions'])}") print() all_plugins = fetch_all_plugins(base_url, auth_headers) # Restore is inherently strict: disable anything not in the snapshot # unless the caller asked only to enable (enable_only=True). to_enable, to_disable, unmatched_names = compute_plugin_updates( all_plugins, enable_names=snapshot_enable_list, disable_names=[], strict=not enable_only, enable_only=enable_only, disable_only=disable_only, ) already_correct_count = max( 0, len(snapshot_enable_list) - len(to_enable) - len(unmatched_names) ) _print_update_plan(to_enable, to_disable, already_correct_count) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched_names(unmatched_names) return if not skip_snapshot and (to_enable or to_disable): auto_save_snapshot(all_plugins, include_version) error_count = _execute_sync(base_url, auth_headers, to_enable, to_disable, use_bulk) print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {error_count}") _report_unmatched_names(unmatched_names) # Main def main() -> None: import argparse parser = argparse.ArgumentParser( prog="plugin_manager.py", description="Manage Sisense plugin states via the REST API.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Plugin names accept the short form (SwitchDimension) or the full\n" "form (plugin-SwitchDimension) in both CLI args and config.yaml.\n" "\n" "Default behavior: enable plugins in enable_plugins, disable plugins\n" "in disable_plugins, leave everything else unchanged.\n" "Use --strict to also disable any enabled plugin not in enable_plugins.\n" "\n" "A snapshot is saved automatically before each run that\n" "modifies state. Use --skip-snapshot to suppress this." ), ) # Universal flags parser.add_argument( "--config", default=str(DEFAULT_CONFIG_PATH), metavar="FILE", help="Path to config.yaml (default: config.yaml next to this script)", ) parser.add_argument( "--dry-run", action="store_true", help="Preview what would change; make no API calls and save no snapshot", ) parser.add_argument( "--no-bulk", action="store_true", help=( "Send one API call per plugin instead of a single batch PATCH. " "Slower but easier to diagnose partial failures. " "Overrides sisense.bulk in config." ), ) parser.add_argument( "--skip-snapshot", action="store_true", help="Do not automatically save a snapshot before this run", ) parser.add_argument( "--log-version", action="store_true", help=( "Record each plugin's version field (from the API response) in any " "snapshot saved during this run (manual --save-snapshot or auto saved snapshot)" ), ) parser.add_argument( "--strict", action="store_true", help=( "Also disable every currently enabled plugin that is not listed in " "enable_plugins. Without this flag, only plugins in disable_plugins are " "disabled; everything else is left alone. " "Always active for --restore-snapshot." ), ) # Direction flags (mutually exclusive) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( "--enable-only", action="store_true", help="Run only the enable step; skip all disables", ) direction_group.add_argument( "--disable-only", action="store_true", help="Run only the disable step; skip all enables", ) # Mode (mutually exclusive) mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument( "--enable-one", metavar="PLUGIN", help=( "Enable exactly one plugin by name and disable all currently enabled " "others. Accepts the short or full plugin name." ), ) mode_group.add_argument( "--save-snapshot", metavar="NAME", nargs="?", const="__auto__", help=( "Save the current plugin state as a named snapshot. " "Omit NAME for an auto generated timestamp filename. " "Combine with --log-version to also record versions for each plugin." ), ) mode_group.add_argument( "--list-snapshots", action="store_true", help="List all saved snapshots with creation date and enabled plugin count", ) mode_group.add_argument( "--restore-snapshot", metavar="NAME", nargs="?", const="__latest__", help=( "Restore plugin state to exactly match a saved snapshot. " "Omit NAME to use the most recently created snapshot. " "Always strict (disables plugins not in snapshot) unless --enable-only." ), ) args = parser.parse_args() # --list-snapshots reads local files only; no config or API connection needed if args.list_snapshots: cmd_list_snapshots() return # Load config and connect config = load_config(Path(args.config)) sisense_config = config.get("sisense", {}) base_url = sisense_config.get("url", "").rstrip("/") api_token = sisense_config.get("token", "") if not base_url or not api_token: print("config.yaml must include sisense.url and sisense.token.") sys.exit(1) auth_headers = make_auth_headers(api_token) use_bulk = sisense_config.get("bulk", True) and not args.no_bulk # Support both 'enable_plugins' (current key) and 'plugins' (legacy key) enable_names = config.get("enable_plugins") or config.get("plugins", []) disable_names = config.get("disable_plugins", []) # Dispatch if args.enable_one: cmd_enable_single_plugin( base_url, auth_headers, args.enable_one, dry_run=args.dry_run, use_bulk=use_bulk, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) elif args.save_snapshot is not None: snapshot_name = ( datetime.now().strftime("snapshot_%Y%m%d_%H%M%S") if args.save_snapshot == "__auto__" else args.save_snapshot ) cmd_save_named_snapshot( base_url, auth_headers, snapshot_name, args.log_version, ) elif args.restore_snapshot is not None: snapshot_name = ( latest_snapshot_name() if args.restore_snapshot == "__latest__" else args.restore_snapshot ) cmd_restore_from_snapshot( base_url, auth_headers, snapshot_name, dry_run=args.dry_run, use_bulk=use_bulk, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) else: # Default: sync instance state to config if not enable_names and not disable_names: print("config.yaml must include at least one entry in 'enable_plugins'.") sys.exit(1) cmd_sync_to_config( base_url, auth_headers, enable_names, disable_names, dry_run=args.dry_run, use_bulk=use_bulk, strict=args.strict, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) if __name__ == "__main__": main()

      Jeremy Friedel
      Jeremy FriedelPosted 1 month ago • Last reply 1 month ago
      1
               
      • TroubleshootingChevronRightIcon

      JumpToDashboard - Troubleshooting the most common configuration issues

                               

      JumpToDashboard - Troubleshooting the most common configuration issues This article provides possible solutions to the most common configuration issues with the JumpToDashboard plugin.  Please review the symptom of the issue first (what error/behavior you experience with the JumpToDashboard plugin) and then follow the solution instructions . If this doesn’t solve your issue, feel free to contact our Support Team describing your issue in detail. Symptoms: A target (usually with “_drill” in prefix) dashboard disappeared for non-owner users in the left-hand side panel. Solution: this behavior could be intended and is controlled by the JumpToDashboard parameter called “ hideDrilledDashboards ”. To make the dashboard visible for the non-owners, please check the following: 1. Log in as a dashboard owner and find the dashboard in question in the left-hand side panel. Click the 3-dots menu and make sure it’s not hidden: 2. If it’s not hidden by the owner intentionally, then navigate to the Admin tab > System Management (under Server & Hardware) > File Management > plugins > jumpToDashboard > js > config.js and check if hideDrilledDashboards set to true. If so, then change it to false and save the changes in the config file. 3. Wait until the JumpToDashboard plugin is rebuilt under the Admin tab > Server & Hardware > Add-ons page and ask your user to refresh the browser page to check if a drill dashboard appears on the left-hand side panel. Symptoms: No "Jump to dashboard" menu appears in a widget edit mode clicking the 3-dots menu.  Solution: there could be different reasons for such behavior so check the most common cases below: Double-check if the JumpToDashboard plugin is enabled under the Admin tab > Server & Hardware > Add-ons page.  Make sure that both dashboards (parent and target) are based on the same ElastiCube. By default, the JumpToDashboard plugin has sameCubeRestriction : true in the config.js file that prevents the ‘jump to’ menu from appearing when a drill dashboard uses a different data source. Check that the prefix you used for the drill dashboard creation is correct. It could be changed in the config.js file. By default, it uses “_drill”: Symptoms: when clicking on a widget that should open a drill dashboard, nothing happens.  Solution : in such cases, we recommend opening your browser console (for example, F12 for Chrome > Console tab) to see if there are any errors that could indicate the issue.  For example, a 403 error in the console indicates that the target dashboard is not shared with the user who is experiencing the issue. To fix it, login as an owner of the drill dashboard and share it with the relevant user or group. Symptoms: when clicking on a widget to get the drill dashboard you get a 404 error. Solution: This issue usually happens when the target/drill dashboard is removed from the system. In order to fix it, please follow the steps below: Log in to the system as an owner. Find the parent widget and open it in edit mode.  Click the 3 dots menu > choose the ‘Jump to dashboard’ menu and select any other dashboard that exists in the system. Press Apply and publish the changes to other users.  Note: if you need just to remove a drill dashboard that doesn’t exist from this widget and not substitute it with another one, try the following: a fter you choose a new drill dashboard, just unselect it after that and then save the changes.  If the jump to dashboard menu doesn’t appear for this widget, try to create a new temporary dashboard with “_drill” in the prefix and do the same.   Symptoms:  The drill dashboard is not opening for some viewers. Solution:  republish the drill dashboard to make sure the updated version is delivered to all end users. Additional Resources: JumpToDashboard Plugin - How to Use and Customize

      Liliia Kislitsyna
      Liliia KislitsynaPosted 2 years ago • Last reply 11 months ago
      2
               
      • TroubleshootingChevronRightIcon

      Troubleshooting Compose SDK Setup Issues

                       

      Troubleshooting Compose SDK Setup Issues Below are some issues you may run into when setting up a TypeScript+React app to use Compose SDK packages and how you may troubleshoot them. Most of them are related to the app configurations as opposed to SDK-known issues, which are listed here . If your issue is not on the list or the solution/workaround does not work for you, please leave a comment below. We will verify and update the list accordingly. #          Action          Error Message/Behavior                  Solution/Workaround   1 yarn install RequestError: unable to get local issuer certificate Turn OFF Internet Security in Zscaler 2 yarn install Invalid authentication (as an anonymous user) Follow https://gitlab.sisense.com/SisenseTeam/compose-sdk-monorepo/-/blob/master/quickstart-public.md#installing-the-sdk-packages to configure personal access token for GitLab Packages Registry. Also, try deleting yarn.lock and running yarn cache clean --all before running yarn install again 3 yarn install No @sisense/sdk* modules installed in node_modules Consequently, the CLI command fails to generate the data model without Check if Yarn Plug'n'Play is enabled. One sign is the creation of pnp.cjs in the root folder after yarn install. To fix, add nodeLinker: node-modules to .yarnrc.yml in the root folder and run yarn install again. 4 CLI command using npx No output in the console and the data model file is not generated This is also related to Yarn Plug’n’Play being enabled. This feature is available in Yarn 4.x, which is not a stable version. To fix: switch Yarn to stable version 3.x by running command yarn set version stable run yarn -v to make sure version 3.x is used. run yarn install to install packages again. run npx command again. 5 CLI command Error connecting to [Sisense URL] TypeError: fetch failed Turn OFF Internet Security in zscaler

      Sisense User
      Sisense UserPosted 2 years ago • Last reply 2 years ago
      1
               
      • TroubleshootingChevronRightIcon

      Basic Sisense troubleshooting using logs and X-Request-Id | Linux OS

               

      Basic Sisense troubleshooting using logs and X-Request-Id | Linux OS This article describes the way a user can track a request generated by the Sisense application inside log files for troubleshooting purposes. Every API request generated by Sisense within the browser includes a parameter in the Request Header called "X-Request-Id" containing a GUID. This GUID is added to each back-end request generated as the result of the initial request.  Using "X-Request-Id" and application logs we can track the whole process and see the point at which certain process fails and potentially why it fails. An example would be a simple PDF export of the dashboard: Now, let's grep the Sisense application logs using the X-Request-Id: cdf90ea5-0395-488e-969a-da509233bc5c sisense@node1 : ~ $ cd /var/log/sisense/sisense/ sisense@node1 : /var/log/sisense/sisense $ cat combined.log| grep "cdf90ea5-0395-488e-969a-da509233bc5c" The result shows us the whole pass of the request which includes identifying the user requesting the export, getting the user, dashboard, and widget details from the application database, generating the data for the PDF, exporting the file to the storage service, and downloading it back to the requester browser. If this request fails at any point, the relevant error message would be printed which usually helps to identify the service and potential root cause for the failure. In case the information found in the logs is not getting the issue resolved, it is recommended to capture the information by either downloading the X-Request-Id and combined.log file or saving the grep output to the file and sending the information to Technical Support for further troubleshooting.  Relevant articles: https://community.sisense.com/t5/knowledge/troubleshooting-guide-general-website-loading-issues/ta-p/9455 https://community.sisense.com/t5/knowledge/performance-issues-troubleshooting/ta-p/8946 https://community.sisense.com/t5/knowledge/email-troubleshooting-sisense-7-2/ta-p/8941 https://community.sisense.com/t5/knowledge/troubleshooting-faq/ta-p/8899

      OlehChudiiovych
      OlehChudiiovychPosted 2 years ago
      0
               
      • Sisense AdministrationChevronRightIcon

      Data Groups In-depth

               

      Data groups are way to limit and control the resources of your instance, avoid Out-Of-Memory and Safe Mode exceptions.   First I would like to describe how Sisense works on a general level. 1. When a user opens the dashboard for the first time, Sisense starting (warming up) the Query pod of the cube where it calculates the results and response to the widget/dashboard. (query pod e.g. "ec-SampleCube- qry " ) 2. Query pod calculates the results and returns them to the dashboard AND saves the results in the RAM for further re-use by other users. 3. When another user opens the same dashboard with the same filters, the same query will be sent, the query pod will not calculate the results instead it will take ready results from the memory, which will speed up the dashboard load. 4. If the user changes a filter, the new query will be sent to the query pod where it will be re-calculated, returned to the dashboard, and saved for further use by other users, and so on.    (NOTE, if Data Security is applied on the cubes, the query results from another user will not be re-used as Sisense adds additional Joins to apply the Data Security that makes every query unique hence it is calculated every time.)   Eventually, a dashboard could (in case of M2M, heavy formulas, complex aggregations, etc.) occupy the RAM of the entire server, which could trigger Out-Of-Memory issues, impact other ec-qry (dashboard) pods from starting and impacting the overall performance of the server.   To avoid this from happening you can limit resources using the Data groups. Now let's check the Data Group settings. Followed by tips on best practices.   Data group settings are available from Admin tab -> Data group at the left-hand side menu  Main Section Group Name - The name of the group that will be reflected in the Data Group list. Build Node(s) - In the case of Multi-node, this is where you should specify the node where the Elasticube will be built. It is crucial to specify the nodes, otherwise, Multi-node capabilities would not be used. In the case of a Single node, the same server is responsible for the build and for the query as well, so it will be the one server to rule them all. Query Nodes - also, where you need to specify the nodes for Query purposed (dashboards) mostly for multinode, in case of Single node it should be the same server as for Build. In the case of multinode, this is where you will specify the node where the ec-ElastiCube-qry pod would run. Needed for redundancy and parallelism.  ElastiCubes - the list of the elasticube to which the Data Group limitations would be applied. Note that you will apply the same settings for each of the cubes in a group. This means limiting the Build to 8 GB  will give 8 GB to each build pod (ec-Elasticue-bld) in the group and not to the entire group.  Instances - the number of query pods created per elasticube when the dashboard is in use. Increasing the number of instances will improve (reduce execution time) the query processing time. This is because the query execution will now be shared between multiple instances. After calculations, the results would not be shared between pods. Increasing the number of instances could help in solving the OOM issues as when one of the pods would be restarted the other will hold the queries. The RAM and CPU limitations would be applied to each of the instances, so if you will limit the RAM for the query to 5 GB you will allow using 5 GB for each instance.   Note: In case the number of instances would be set to zero, it will enable the IDLE timeout that will stop the elasticube (delete query pod) in case the Elasticube was not used for 30 minutes.  Remember that it will take time to start the cube after it was stopped, however, when it's stopped it's not using any resources. This is useful in case the dashboard is not used often and there is no need to keep the results of the queries.  UPDATE: starting from L2022.5 the IDLE option moved from "Instances" and now have own toggle in the bottom of the Data Group Settings. Change IDLE time 1. Go to the Configuration manager . Available from Admin tab -> System Settings. and on the top right corner 2. Scroll down to the bottom of the page  3. Press on "Show Advanced" 4. Expand "Advanced Management Params" 5. Edit "Stop Unused Datasource (minutes)" to desired 6. Save changes. Note settings would be applied on elasticube rebuild. Secondary Section ElastiCube Query Recycler - when disabled, the query pod will store the results of query execution, and on each request will re-calculate the results. Could be useful in the case of Data Security when queries could not be re-used by another user. Or in case you testing the cube and do not want to use much RAM. Connector Running Mode - will create the connector pod inside the build pod, which will increase the build time but will make the build process more stable. Will require more RAM. Should be used for debugging purposes only. Index Size (Short/Long) - Long should be used if the cube has more than 50M rows or in case the text fields have tons of symbols. In this case, Sisense will use another indexing (x64) Simultaneous Query Executions - Limits the number of connections that the query Pod will open to the ec-qry Pod for running/executing queries. In case your widget contains heavy formulas it is worth reducing the number to make the pressure lower. Used when experiencing out-of-memory (OOM) issues (java, not Safe-Mode). Parallelization is a trade-off between memory usage & query performance.  8 is optimal amount of queries         NOT RECOMMENDED changing without Sisense Support/Architect advice. Query Timeout (Seconds) - how long dashboard will wait for a response with the results of a query  from the query pod % Concurrent Cores per Query - related to Mitosis and Parallelism technology. To minimize the risk of OOM, set this value to the equivalent of 8 cores. e.g. if 32-core, set to 25; for 64 cores set to 14; the value should be an even number.  Can be increased up to a maximum of half the total cores - i.e. treat the default as the recommended max that can be adjusted down only . Change it when experiencing out-of-memory (OOM) issues. Query Nodes Settings (for ec-qry Pods, Dashboards)  Reserved Cores - number of free cores without which the pod will not start. Meaning if at the moment when the pod should be started ( the dashboard has been opened) Sisense checks if there's 1 free core. If it is - Sisense will start qry pod. If not - will not start. Also, the setting will reserve this core only for the cube, and no one else could use this core(s) Max Cores - Maximum cores allowed to be used by the qry pod. Reserved RAM (MB ) - Same as Reserved Cores but regarding the RAM used by Dashboards. Needed RAM to start the qry pod, also it reserves the RAM so the reserved will be used only by this qry-pod of the cube. If other cubes would require RAM while the build/dashboard use happens, they will not get it from the reserved. Max RAM (MB) - Maximum RAM that is allowed to be used by the qry pod ( dashboards). By each of the query instances if were increased in the Instances . Please note that at 85% of the pod usage OR overall server RAM will cause Safe-Mode exception. that in case of Dashboards qry pod) will delete and start the pod again. At the Safe Mode, the users of a dashboard would see the error of a Safe Mode and the qry pod will be deleted and started again. So the next dashboard refresh will bring the data. I would recommend setting the MAX ram to 40GB, this is the max reasonable RAM, if you will see the safe mode error with 40 GB  limits - it's time to check the dashboard for optimization and/or many-to-many. Please note that the limit is very dependent on the size of the elasticube, Data Security and formulas you're using. and even 1 gb size on a disk cube can use 40 GB of RAM, when 100GB size on a disk, cube might use 10 GB of RAM, it's ok if elasticube will use 2-3 times its size on a disk. However, in case the elasticube will use 40 GB I would recommend checking some optimization for example in case the RAM is used due to heavy calculations on the widget, you can try to move the calculation to the elastcube build level i.e. create custom table with needed calculation, so they will be performed while the cube is build and on a dashboard - you will use ready results. Yes, in this case, it will use more RAM on a build, but it's will be one time. Please check more best practices and optimizations at the end of the article. Build Nodes Settings (for ec-bld Pods ) Reserved Cores - number of free cores without which the pod will not start. Meaning if at the moment when the pod should be started ( the Build started) Sisense check if there's 1 free core. if it is - Sisense will start bld pod. if not - will not start. Max Cores - Maximum cores allowed to be used by the BLD pod. Reserved RAM (MB) - Amount of free RAM to start the build. Could be used if you know that the build will take 30 GB for example and if not, it will fail. So knowing that it will fail anyway, no point to start it and use RAM, if any way it will fail. Or in order to "reserve" the RAM so no one else will use it when the build will happen and you will be sure that it will finish successfully. Max RAM (MB) - the maximum RAM that is allowed to be used by a cube when it builds.    Additional Information User label - Assigns labels to nodes in the data group. This is useful for scaling your nodes. For example, if you have implemented auto-scaling you can use these labels to scale up the nodes. You can check further information on this article .  Storage Settings Interim Build on Local Storage - Enable to build ElastiCubes on the local storage instead of shared storage. Should be used to troubleshoot the issues on a Multinode where cubes are usually stored on shared storage so that all nodes would have access to it and for redundancy as well. On single-node its always build to local storage as single doesn't have shared storage) Store ElastiCube Data on S3 Enable -  Enable to save your ElastiCubes on S3 if you have configured S3 access in your system configuration. Reflected (cannot be changed from there) in Configuration manager->Management   Query cube from local storage -  Enable to query the cube from the local storage in case Interim Build on Local Storage is enabled, for Multinode. Using local storage can decrease the time required to query the cube, after the first build on local storage. Set as default - apply to set the group as default. In this case, all new cubes would be created with settings of the default Data Group.                                                                        Best Practices   1. Choose the strategy that is more important in your case, successful build or dashboard loads without Safe-Mode exception.  a) Successful build - plan the schedule so the cubes would not overlap each other. as well as set the unlimited (-1) in the Max RAM for Build Nodes and limit the Query pods to the amount that will save RAM for a successful build b) Dashboard priority - in this case, you need to understand the limits that the build will take. Run the build with unlimited, and check Grafana how much it will use . Add 15% ( Safe-Mode ) and set up the Build limits. in this case, the build will not use more than limited and the SafeMode exception will let you know that the Data is increasing, and that will give more control of your environment   2. Setup reasonable limitations for Query pod .  Remember as much as you will limit as much it will take to release the RAM. Also, the reasonable MAX limits in most cases are 40 GB , in case the query pod uses more it is worth checking the dashboards in order to optimize the widgets etc. However, ideal middle-grade cubes should not use more than 10 GB of RAM. Unfortunately, it is not possible to correlate and predict using the size of the cubes it takes on storage VS how much RAM it will use as it depends on the formulas you're using in the widgets. The aggregation from 1 table and pivot with many JOINs will use much more RAM using the same amount of data from cubes. Also, check the Performance Guide     3. Create different groups for different purposes , for example, create a "test" group and limit the resources so that even created by mistake m2m will not overuse the RAM. Create Small Scale group for Demo cubes/dashboards so that under no circumstances they will not use RAM more than you will limit (1 GB for example)    4. Increase the number of instances if RAM is sot an issue but the dashboards load slowly.   5. Consider autoscaling capabilities. For example, you see that the RAM/CPU spike and issues happened only on weekends when you rebuild all your cubes. And the current amount of RAM/CPU is not enough only at this day/week/hour. with autoscaling, Sisense will create an additional node that will handle the additional load and will be scaled down when not needed, for example, IF RAM load >= 80% - start a new node. If RAM get lower 80% scale down the pod. This is cheaper rather upgrade the entire server if at another period of time the RAM is not needed.      

      intapiuser
      intapiuserPosted 3 years ago
      0
               
      • TroubleshootingChevronRightIcon

      Using Grafana to troubleshoot performance issues

               

      Overview Out of the box, Sisense running on Linux comes with embedded monitoring tool Grafana . Sisense created a few dashboards that can be helpful in order to troubleshoot the performance issues. Grafana can be accessed from Admin page->System Settings-> Monitoring (in top right corner) or by https://your_web_site/app/grafana/       The dashboards list is available by navigating on the left-hand side menu on a 4 square button-> manage Navigation Grafana has some base controls that makes it straightforward to gather info about the system over a period of time.    Within a dashboard you will see similar headers to the following: 1: Filter dropdowns - such as namespace (by default, Sisense components run in the sisense namespace), pods, and node(s). 2: Time period - can be configured to be relative or absolute (according to browser timezone) 3: Refresh - can manually toggle the refresh or set a refresh rate   Within a visualization, you can hover over particular lines or click on one or more values   Click and drag over a time period to zoom in on the time period for more accurate values   For more information, check out the official Grafana docs - Working with Grafana dashboard UI Available Dashboards We will focus our troubleshooting using 4 dashboards: Nodes All Pods per namespace Kubernetes / Compute Resources / Namespace (Workloads) Import the dashboard 12928 Nodes Dashboard This dashboard is useful for troubleshooting overall node performance during a period of time. It can tell us what the CPU, RAM, Disk I/O, and networking usage of the machine during a period of time.   Use the 'instance' drop down to select which node to examine. All Pods per Namespace Dashboard The dashboard header will allow you to filter the pods/nodes, setup the timeframe, specify refresh/autorefresh:   The dashboard has 3 widgets (CPU, RAM and Network) and will show the load of a server pod-by-pod (separately) overlapping each other  This is useful when checking what the RAM consumption of a pod is.    Please note this dashboard not does not show overall RAM consumption by default, it will show the RAM of each pod individually.  Click to check how to add Total: In Grafana (hosted on /app/grafana), under dashboard named  all pods per namespace  , two widgets ( Cpu Usage, Memory Usage)  need to add the sum of all Pods. This is mandatory information when troubleshooting resource pressure situations.   Add the following metric to the memory widget: sum (container_memory_working_set_bytes{job="kubelet", pod_name=~"$pod", container_name!=""})   We can then see more clearly the state of the machine (RAM in this case):   using the Pod filter you can filter all the query or build POD by typing "qry" or "bld" in a filter and selecting needed Pods:   Hover over on the widget item to see the name of a POD    Drag and drop to select needed timeframe:   Kubernetes / Compute Resources / Namespace (Workloads) This dashboard has a lot more widgets and will show you the total usage of RAM, CPU etc Useful when need to see total usage to identify Safe-Mode trigger . For multinode see the manual   This dashboard additional widgets that can be helpful in monitoring your server performance, network usage, etc. Sisense Cluster Detail Dashboard (#12928) This dashboard is included by default in many recent versions of Sisense on Linux. This dashboard has a lot of pre-set-up widgets that will show you pods, cluster, Drive, RAM etc usage.    Before you try to import this dashboard, check to see if the dashboard is already in your Grafana dashboard menu.   If your instance is on an older release or you would like to import a dashboard from other users, follow the steps below: 1. Click the 4 square menu icon, then Manage dashboard 2. Press on import dashboard in the top right corner  3. Specify the number of a dashboard 12928 (or other dashboard number) and press load  4. Select Logs Data source "Prometheus" 5. Click import and enjoy  

      intapiuser
      intapiuserPosted 3 years ago
      0
               
      • TroubleshootingChevronRightIcon

      SSO Linux Troubleshooting

               

      SSO Troubleshooting for Linux is available via Sisense public documentation. https://documentation.sisense.com/docs/using-single-sign-on-to-access-sisense

      intapiuser
      intapiuserPosted 3 years ago
      0
               
      • TroubleshootingChevronRightIcon

      How much RAM used by a dashboard/widget (Linux)

               

      In order to control the RAM usage and to set up proper Data Group Limits, you would like to know how much RAM is used by a dashboard/widget. In Linux, you can use Grafana , in Windows, you can use Performance Monito r 1. Make sure that no one else is using the dashboard connected to the same cube as the dashboard that you checking 2. Open three separate tabs in your browser:       - Dashboard you would like to check        - Grafana       - Elasticube Data Tab  3. Restart the query pod by: - In Data Tab stop the cube that is a datasource of the Dashboard - wait for 5-10 sec and start the cube  (do not use the "restart" option, to get the correct result you need to stop and then start the cube)  4. In Grafana "All pods per namespace" dashboard filter for a newly created query pod of the cube by "ec-cube_Name" or by "qry" enable autorefresh for more convenience and check current usage 5. Refresh the tab with the dashboard. It will send the request to query pod  to calculate the result 6. Check the RAM usage of a POD  in Grafana      Using the same technique you can check widget usage, to do so open the required widget by pressing on edit (pencil) button, instead of the entire dashboard and you will be able to restart only the widget page that will send the query only from a particular widget. check the video demo https://www.loom.com/share/227b3acf97dd4c4fa74156642a857258   T he main reasons of the performance issues are: - Heavy formulas are used inside widgets. In this case, you can move the calculations to a cube level i.e. create a custom table/column with formulas so they would be calculated when build, and on a dashboard, you will use results and aggregations etc - Data Security . In the case of data security, the main issue is that the results of the queries that Sisense usually re-uses between customers cannot be reused. Data Security adds additional join with Data Security that makes sisense think that the query is unique and calculate result again. - Many-to-many Many-to-many The many-to-many relationship is inflating your data due to duplication resulting from the Cartesian product generated from this relationship.   Sisense recommend installing the JAQLine plugin in order to inspect the connections between the tables: For more information, see the following articles: Many to Many Relationships Star Schema Multi Fact Schema Also, please check a comprehensive guide we compiled to troubleshoot the performance issue.

      intapiuser
      intapiuserPosted 3 years ago
      0