Compare commits

...

12 Commits

Author SHA1 Message Date
nityanandagohain
99edf96910 fix: use new error in pipelines handler 2025-07-11 17:38:27 +05:30
Nityananda Gohain
552d44d208 chore: send email on role update (#8489)
* chore: send email on role update

* fix: minor changes

* fix: update template

* fix: minor changes

* fix: return updated user
2025-07-10 15:17:04 +00:00
SagarRajput-7
497315579f chore: added got at 11.8.5 patch to fix image-webpack-loader vulnerability (#8500) 2025-07-10 20:22:39 +05:30
SagarRajput-7
bfaac15ccb chore: replace image-webpack-loader (deprecated) with image-minimizer-webpack-plugin (#8498)
* chore: replace image-webpack-loader (deprecated) with image-minimizer-webpack-plugin

* chore: used sharp

* chore: remove got resolution
2025-07-10 19:17:02 +05:30
SagarRajput-7
5e18be6a23 chore: added got at 11.8.5 patch to fix image-webpack-loader vulnerability (#8493) 2025-07-10 16:07:10 +05:30
Yunus M
1793706f87 feat: show ingestion keys to self hosted users (#8490) 2025-07-10 14:51:53 +05:30
aniketio-ctrl
da2a3c738a fix(aws-elastic-cache): corrected variable query for elastic cache (#8487)
* fix(aws-elastic-cache): corrected variable query for elastic cache overview.json

* fix(aws-elastic-cache): corrected variable query for elastic cache overview.json

---------

Co-authored-by: Piyush Singariya <piyushsingariya@gmail.com>
2025-07-09 10:21:15 +00:00
primus-bot[bot]
d17dab9a1d chore(release): bump to v0.89.0 (#8482) 2025-07-09 12:06:47 +05:30
Srikanth Chekuri
88b75d4e72 fix(apdex): use right metric name for metadata (#8463) 2025-07-09 09:08:40 +05:30
Sahil Khan
6327ab5ec6 fix: allowed user to select text in json body field in log details (#8450) 2025-07-08 21:28:05 +05:30
Sahil Khan
5b09490ad7 fix: trace details v2 ui bugs (#8448) 2025-07-08 13:51:40 +00:00
Nageshbansal
b50127b567 feat(statsreporter): add railway platform detection (#8467) 2025-07-08 13:01:21 +00:00
25 changed files with 672 additions and 1136 deletions

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.88.1
image: signoz/signoz:v0.89.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.88.1
image: signoz/signoz:v0.89.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.88.1}
image: signoz/signoz:${VERSION:-v0.89.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.88.1}
image: signoz/signoz:${VERSION:-v0.89.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -213,7 +213,9 @@
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"image-webpack-loader": "8.1.0",
"image-minimizer-webpack-plugin": "^4.0.0",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
@@ -230,6 +232,7 @@
"redux-mock-store": "1.5.4",
"sass": "1.66.1",
"sass-loader": "13.3.2",
"sharp": "^0.33.4",
"ts-jest": "^27.1.5",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
@@ -254,6 +257,7 @@
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0"
"prismjs": "1.30.0",
"got": "11.8.5"
}
}

View File

@@ -38,6 +38,7 @@ import dayjs from 'dayjs';
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { isNil, isUndefined } from 'lodash-es';
import {
@@ -167,6 +168,8 @@ function MultiIngestionSettings(): JSX.Element {
const [totalIngestionKeys, setTotalIngestionKeys] = useState(0);
const { isEnterpriseSelfHostedUser } = useGetTenantLicense();
const [
hasCreateLimitForIngestionKeyError,
setHasCreateLimitForIngestionKeyError,
@@ -293,7 +296,7 @@ function MultiIngestionSettings(): JSX.Element {
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
isError: isErrorDeploymentsData,
} = useGetDeploymentsData(true);
} = useGetDeploymentsData(!isEnterpriseSelfHostedUser);
const {
mutate: createIngestionKey,
@@ -1308,7 +1311,8 @@ function MultiIngestionSettings(): JSX.Element {
{!isErrorDeploymentsData &&
!isLoadingDeploymentsData &&
!isFetchingDeploymentsData && (
!isFetchingDeploymentsData &&
deploymentsData && (
<div className="ingestion-setup-details-links">
<div className="ingestion-key-url-container">
<div className="ingestion-key-url-label">Ingestion URL</div>

View File

@@ -1,6 +1,9 @@
import styled from 'styled-components';
export const TitleWrapper = styled.span`
user-select: text !important;
cursor: text;
.hover-reveal {
visibility: hidden;
}

View File

@@ -71,8 +71,13 @@ function BodyTitleRenderer({
onClick: onClickHandler,
};
const handleTextSelection = (e: React.MouseEvent): void => {
// Prevent tree node click when user is trying to select text
e.stopPropagation();
};
return (
<TitleWrapper>
<TitleWrapper onMouseDown={handleTextSelection}>
<Dropdown menu={menu} trigger={['click']}>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>

View File

@@ -11,6 +11,18 @@
}
}
.selectable-tree {
.ant-tree-node-content-wrapper {
user-select: text !important;
cursor: text !important;
}
.ant-tree-title {
user-select: text !important;
cursor: text !important;
}
}
.table-view-actions-content {
.ant-popover-inner {
border-radius: 4px;

View File

@@ -53,7 +53,12 @@ const convert = new Convert();
// Memoized Tree Component
const MemoizedTree = React.memo<{ treeData: any[] }>(({ treeData }) => (
<Tree defaultExpandAll showLine treeData={treeData} />
<Tree
defaultExpandAll
showLine
treeData={treeData}
className="selectable-tree"
/>
));
MemoizedTree.displayName = 'MemoizedTree';

View File

@@ -3,9 +3,11 @@ import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
import useErrorNotification from 'hooks/useErrorNotification';
import { useParams } from 'react-router-dom';
import { FeatureKeys } from '../../../../../constants/features';
import { useAppContext } from '../../../../../providers/App/App';
import { WidgetKeys } from '../../../constant';
import { IServiceName } from '../../types';
import ApDexMetrics from './ApDexMetrics';
import { metricMeta } from './constants';
import { ApDexDataSwitcherProps } from './types';
function ApDexMetricsApplication({
@@ -18,7 +20,19 @@ function ApDexMetricsApplication({
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { data, isLoading, error } = useGetMetricMeta(metricMeta, servicename);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const signozLatencyBucketMetrics = dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm;
const { data, isLoading, error } = useGetMetricMeta(
signozLatencyBucketMetrics,
servicename,
);
useErrorNotification(error);
if (isLoading) {

View File

@@ -1 +0,0 @@
export const metricMeta = 'signoz_latency_bucket';

View File

@@ -215,6 +215,27 @@ export function SpanDuration({
setHasActionButtons(false);
};
// Calculate text positioning to handle overflow cases
const textStyle = useMemo(() => {
const spanRightEdge = leftOffset + width;
const textWidthApprox = 8; // Approximate text width in percentage
// If span would cause text overflow, right-align text to span end
if (leftOffset > 100 - textWidthApprox) {
return {
right: `${100 - spanRightEdge}%`,
color,
textAlign: 'right' as const,
};
}
// Default: left-align text to span start
return {
left: `${leftOffset}%`,
color,
};
}, [leftOffset, width, color]);
return (
<div
className={cx(
@@ -270,7 +291,7 @@ export function SpanDuration({
<Typography.Text
className="span-line-text"
ellipsis
style={{ left: `${leftOffset}%`, color }}
style={textStyle}
>{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text>
</Tooltip>
</div>
@@ -311,6 +332,16 @@ function getWaterfallColumns({
/>
),
size: 450,
/**
* Note: The TanStack table currently does not support percentage-based column sizing.
* Therefore, we specify both `minSize` and `maxSize` for the "span-name" column to ensure
* that its width remains between 240px and 900px. Setting a `maxSize` here is important
* because the "span-duration" column has column resizing disabled, making it difficult
* to enforce a minimum width for that column. By constraining the "span-name" column,
* we indirectly control the minimum width available for the "span-duration" column.
*/
minSize: 240,
maxSize: 900,
}),
columnDefHelper.display({
id: 'span-duration',

View File

@@ -110,7 +110,7 @@ function SettingsPage(): JSX.Element {
item.key === ROUTES.INTEGRATIONS ||
item.key === ROUTES.API_KEYS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.SHORTCUTS
item.key === ROUTES.INGESTION_SETTINGS
? true
: item.isEnabled,
}));
@@ -120,7 +120,11 @@ function SettingsPage(): JSX.Element {
// eslint-disable-next-line sonarjs/no-identical-functions
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled: item.key === ROUTES.INTEGRATIONS ? true : item.isEnabled,
isEnabled:
item.key === ROUTES.INTEGRATIONS ||
item.key === ROUTES.INGESTION_SETTINGS
? true
: item.isEnabled,
}));
}
}
@@ -130,9 +134,7 @@ function SettingsPage(): JSX.Element {
updatedItems = updatedItems.map((item) => ({
...item,
isEnabled:
item.key === ROUTES.API_KEYS ||
item.key === ROUTES.ORG_SETTINGS ||
item.key === ROUTES.SHORTCUTS
item.key === ROUTES.API_KEYS || item.key === ROUTES.ORG_SETTINGS
? true
: item.isEnabled,
}));

View File

@@ -119,24 +119,8 @@ const config = {
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
optipng: {
optimizationLevel: 7,
},
gifsicle: {
interlaced: false,
},
},
},
],
test: /\.(jpe?g|png|gif|svg)$/i,
type: 'asset',
},
{
test: /\.(ttf|eot|woff|woff2)$/,

View File

@@ -14,6 +14,7 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
dotenv.config();
@@ -135,24 +136,8 @@ const config = {
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
optipng: {
optimizationLevel: 7,
},
gifsicle: {
interlaced: false,
},
},
},
],
test: /\.(jpe?g|png|gif|svg)$/i,
type: 'asset',
},
{
@@ -212,6 +197,55 @@ const config = {
},
}),
new CssMinimizerPlugin(),
new ImageMinimizerPlugin({
minimizer: [
{
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
jpeg: {
quality: 80,
},
webp: {
lossless: true,
},
avif: {
lossless: true,
},
png: {},
gif: {},
},
},
},
{
implementation: ImageMinimizerPlugin.imageminMinify,
options: {
plugins: [
[
'svgo',
{
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
addAttributesToSVGElement: {
params: {
attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }],
},
},
},
},
},
],
},
],
],
},
},
],
}),
],
},
performance: {

File diff suppressed because it is too large Load Diff

View File

@@ -289,43 +289,6 @@ func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
return
}
existingUser, err := h.module.GetUserByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(w, err)
return
}
// only displayName, role can be updated
if user.DisplayName == "" {
user.DisplayName = existingUser.DisplayName
}
if user.Role == "" {
user.Role = existingUser.Role
}
if user.Role != existingUser.Role && claims.Role != types.RoleAdmin {
render.Error(w, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles"))
return
}
// Make sure that the request is not demoting the last admin user.
// also an admin user can only change role of their own or other user
if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin.String() {
adminUsers, err := h.module.GetUsersByRoleInOrg(ctx, claims.OrgID, types.RoleAdmin)
if err != nil {
render.Error(w, err)
return
}
if len(adminUsers) == 1 {
render.Error(w, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot demote the last admin"))
return
}
}
user.UpdatedAt = time.Now()
updatedUser, err := h.module.UpdateUser(ctx, claims.OrgID, id, &user, claims.UserID)
if err != nil {
render.Error(w, err)

View File

@@ -176,18 +176,69 @@ func (m *Module) ListUsers(ctx context.Context, orgID string) ([]*types.Gettable
}
func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User, updatedBy string) (*types.User, error) {
user, err := m.store.UpdateUser(ctx, orgID, id, user)
existingUser, err := m.GetUserByID(ctx, orgID, id)
if err != nil {
return nil, err
}
traits := types.NewTraitsFromUser(user)
requestor, err := m.GetUserByID(ctx, orgID, updatedBy)
if err != nil {
return nil, err
}
// only displayName, role can be updated
if user.DisplayName == "" {
user.DisplayName = existingUser.DisplayName
}
if user.Role == "" {
user.Role = existingUser.Role
}
if user.Role != existingUser.Role && requestor.Role != types.RoleAdmin.String() {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles")
}
// Make sure that the request is not demoting the last admin user.
// also an admin user can only change role of their own or other user
if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin.String() {
adminUsers, err := m.GetUsersByRoleInOrg(ctx, orgID, types.RoleAdmin)
if err != nil {
return nil, err
}
if len(adminUsers) == 1 {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot demote the last admin")
}
}
user.UpdatedAt = time.Now()
updatedUser, err := m.store.UpdateUser(ctx, orgID, id, user)
if err != nil {
return nil, err
}
traits := types.NewTraitsFromUser(updatedUser)
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traits)
traits["updated_by"] = updatedBy
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Updated", traits)
return user, nil
// if the role is updated then send an email
if existingUser.Role != updatedUser.Role {
if err := m.emailing.SendHTML(ctx, existingUser.Email, "Your Role is updated in SigNoz", emailtypes.TemplateNameUpdateRole, map[string]any{
"CustomerName": existingUser.DisplayName,
"UpdatedByEmail": requestor.Email,
"OldRole": existingUser.Role,
"NewRole": updatedUser.Role,
}); err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}
}
return updatedUser, nil
}
func (m *Module) DeleteUser(ctx context.Context, orgID string, id string, deletedBy string) error {

View File

@@ -2858,11 +2858,11 @@ func (r *ClickHouseReader) GetMetricMetadata(ctx context.Context, orgID valuer.U
WHERE metric_name = $1
AND unix_milli >= $2
AND type = 'Histogram'
AND JSONExtractString(labels, 'service_name') = $3
AND (JSONExtractString(labels, 'service_name') = $3 OR JSONExtractString(labels, 'service.name') = $4)
GROUP BY le
ORDER BY le`, signozMetricDBName, signozTSTableNameV41Day)
rows, err := r.db.Query(ctx, query, metricName, unixMilli, serviceName)
rows, err := r.db.Query(ctx, query, metricName, unixMilli, serviceName, serviceName)
if err != nil {
zap.L().Error("Error while querying histogram buckets", zap.Error(err))
return nil, fmt.Errorf("error while querying histogram buckets: %s", err.Error())

View File

@@ -175,7 +175,7 @@
"multiSelect": false,
"name": "Account",
"order": 0,
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS cloud.account.id FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' GROUP BY cloud.account.id",
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id` FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'aws_ElastiCache_CPUUtilization_max' GROUP BY `cloud.account.id`",
"showALLOption": false,
"sort": "DISABLED",
"textboxValue": "",

View File

@@ -4041,7 +4041,7 @@ func (aH *APIHandler) PreviewLogsPipelinesHandler(w http.ResponseWriter, r *http
req := logparsingpipeline.PipelinesPreviewRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
render.Error(w, err)
return
}
@@ -4050,7 +4050,7 @@ func (aH *APIHandler) PreviewLogsPipelinesHandler(w http.ResponseWriter, r *http
)
if apiErr != nil {
RespondError(w, apiErr, nil)
render.Error(w, apiErr)
return
}
@@ -4072,7 +4072,7 @@ func (aH *APIHandler) ListLogsPipelinesHandler(w http.ResponseWriter, r *http.Re
version, err := parseAgentConfigVersion(r)
if err != nil {
RespondError(w, model.WrapApiError(err, "Failed to parse agent config version"), nil)
render.Error(w, err)
return
}
@@ -4086,7 +4086,7 @@ func (aH *APIHandler) ListLogsPipelinesHandler(w http.ResponseWriter, r *http.Re
}
if apierr != nil {
RespondError(w, apierr, payload)
render.Error(w, apierr)
return
}
aH.Respond(w, payload)
@@ -4164,7 +4164,7 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
req := pipelinetypes.PostablePipelines{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, model.BadRequest(err), nil)
render.Error(w, err)
return
}
@@ -4186,7 +4186,7 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
res, err := createPipeline(r.Context(), req.Pipelines)
if err != nil {
RespondError(w, err, nil)
render.Error(w, err)
return
}

View File

@@ -12,11 +12,12 @@ import (
var (
// Templates is a list of all the templates that are supported by the emailing service.
// This list should be updated whenever a new template is added.
Templates = []TemplateName{TemplateNameInvitationEmail}
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole}
)
var (
TemplateNameInvitationEmail = TemplateName{valuer.NewString("invitation_email")}
TemplateNameUpdateRole = TemplateName{valuer.NewString("update_role")}
)
type TemplateName struct{ valuer.String }
@@ -25,6 +26,8 @@ func NewTemplateName(name string) (TemplateName, error) {
switch name {
case TemplateNameInvitationEmail.StringValue():
return TemplateNameInvitationEmail, nil
case TemplateNameUpdateRole.StringValue():
return TemplateNameUpdateRole, nil
default:
return TemplateName{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid template name: %s", name)
}

View File

@@ -106,6 +106,8 @@ func detectPlatform() string {
return "render"
case os.Getenv("COOLIFY_RESOURCE_UUID") != "":
return "coolify"
case os.Getenv("RAILWAY_SERVICE_ID") != "":
return "railway"
}
// Try to detect cloud provider through metadata endpoints

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<body>
<p>Hi {{.CustomerName}},</p>
<p>We wanted to inform you that your role in the <strong>SigNoz</strong> project has been updated by <strong>{{.UpdatedByEmail}}</strong>.</p>
<p>
<strong>Previous Role:</strong> {{.OldRole}}<br>
<strong>New Role:</strong> {{.NewRole}}
</p>
<p>
Please note that you will need to <strong>log out and log back in</strong> for the changes to take effect.
</p>
<p>
If you were not expecting this change or have any questions, please reach out to your project administrator or contact us at <a href="mailto:support@signoz.io">support@signoz.io</a>.
</p>
<p>Thanks,<br/>The SigNoz Team</p>
</body>
</html>