6 Commits

Author SHA1 Message Date
gorbenko.pavel eaa1c855d9 Мониторинг: логи создания заявок, отправки на согласование и решений согласующих
Tests / frontend-test (pull_request) Successful in 4m39s
saveRequestItem — «Создана заявка» (тип, id, код); writeRoute/writeApprovers — «Заявка отправлена на
согласование» (статус, число согласующих и исполнителей); ApproverService.decide — «Заявка согласована /
возвращена / отклонена / выполнена исполнителем» (новый статус, строка маршрута, комментарий до 500
символов). Пользователь — из токена мониторинга.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:18:18 +05:00
gorbenko.pavel cce5166633 Мониторинг: @tnsu/monitoring-web 0.3.3 — console.error SharePoint не уходит в логи
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:15:27 +05:00
gorbenko.pavel f2c601bf99 Мониторинг: имя сервиса request-portal (как репозиторий), а не requestPortal
Tests / frontend-test (pull_request) Successful in 1m31s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:50:08 +05:00
gorbenko.pavel e204eeab72 Мониторинг: @tnsu/monitoring-web 0.3.1 — патч webpack и «один на страницу» из библиотеки
Tests / frontend-test (pull_request) Successful in 4m36s
config/webpack-patch.json ссылается на патч из пакета (spfx/webpack-patch.cjs), свой
config/webpack-patch/monitoring.js удалён. Флаг monitoringStarted не нужен: повторный initMonitoring
библиотека игнорирует сама.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:34:11 +05:00
gorbenko.pavel 4b09833bc4 Пакет @tnsu/monitoring-web 0.3.0 из реестра tansu
Tests / frontend-test (pull_request) Successful in 4m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:21:23 +05:00
gorbenko.pavel ee78449131 Мониторинг: логи, ошибки и запись экрана портала заявок в monitoring.tnsu.kz
initMonitoring в onInit веб-части (один раз на страницу), service.name = requestPortal, токен — молча
через AAD веб-части (spfxTokenProvider). В package-solution.json запрос Logs.Write к регистрации
мониторинга. Хэш коммита сборки — патч webpack (config/webpack-patch.json): GIT_COMMIT, GITHUB_SHA или git.
Пакет @tnsu/monitoring-web ставится отдельно.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:16:55 +05:00
6 changed files with 28 additions and 80 deletions
+4 -5
View File
@@ -17,8 +17,7 @@
"@microsoft/sp-webpart-base": "1.22.1",
"@pnp/sp": "^4.20.0",
"@pnp/spfx-controls-react": "^3.25.0",
"@pnp/telemetry-js": "2.0.0",
"@tnsu/monitoring-web": "^0.3.4",
"@tnsu/monitoring-web": "^0.3.3",
"@tnsu/ui-kit": "^0.15.0",
"jszip": "~3.8.0",
"react": "17.0.1",
@@ -9910,9 +9909,9 @@
"license": "0BSD"
},
"node_modules/@tnsu/monitoring-web": {
"version": "0.3.4",
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fmonitoring-web/-/0.3.4/monitoring-web-0.3.4.tgz",
"integrity": "sha512-tQe0ZM1tBVO3XtbNFliRaaooqvCX3gb/Q/j2lV6+rYCXprodBZByQ6PLrCu/P0me/rW8KdQ02hu7avyLXXfkoQ==",
"version": "0.3.3",
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fmonitoring-web/-/0.3.3/monitoring-web-0.3.3.tgz",
"integrity": "sha512-jxuGTHA8pI71HZzAl+pDhSKgMY7YA/F8Xiq0U4koTw2w7oWcF8XeRwQ3z6lJUpvpWGn1FLpZOjE9/8SoMBPNrA==",
"license": "UNLICENSED",
"dependencies": {
"@rrweb/rrweb-plugin-console-record": "^2.2.0",
+1 -2
View File
@@ -21,8 +21,7 @@
"@microsoft/sp-webpart-base": "1.22.1",
"@pnp/sp": "^4.20.0",
"@pnp/spfx-controls-react": "^3.25.0",
"@pnp/telemetry-js": "2.0.0",
"@tnsu/monitoring-web": "^0.3.4",
"@tnsu/monitoring-web": "^0.3.3",
"@tnsu/ui-kit": "^0.15.0",
"jszip": "~3.8.0",
"react": "17.0.1",
@@ -9,7 +9,6 @@ import {
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import PnPTelemetry from '@pnp/telemetry-js';
import { initMonitoring, spfxTokenProvider } from '@tnsu/monitoring-web';
import * as strings from 'RequestPortalWebPartStrings';
@@ -105,9 +104,6 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
// Один на страницу: повторный вызов (несколько веб-частей) библиотека игнорирует сама.
initMonitoring({ service: 'request-portal', getAccessToken: spfxTokenProvider(this.context.aadTokenProviderFactory) });
installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения
// Телеметрия PnP (её включает @pnp/spfx-controls-react) шлёт fetch на pnptelemetryproxy.azurewebsites.net
// без catch: где адрес закрыт прокси или блокировщиком, это необработанный отказ промиса в мониторинге.
PnPTelemetry.getInstance().optOut();
initSP(this.context); // единый клиент PnPjs для записи в списки
// Адрес approval-route должен быть выставлен ДО загрузки партиций — они
@@ -1,4 +1,5 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { log } from '@tnsu/monitoring-web';
import { resolveRoute } from './approvalRoute.service';
import { ApprovalListService, IApproverToWrite } from './approvalList.service';
import { IMatrixRow } from './models/IMatrixRow';
@@ -68,6 +69,11 @@ export async function writeApprovers(
await approval.createSteps(requestId, approvers, rowStatus, APPROVAL_PHASE.approval, 1);
if (submitted) {
await approval.activateNextQueued(requestId);
log.info('Заявка отправлена на согласование', {
'request.id': requestId,
'request.status': status,
'route.approvers': approvers.length,
});
}
}
@@ -138,5 +144,11 @@ export async function writeRoute(
if (submitted) {
await approval.activateNextQueued(requestId);
log.info('Заявка отправлена на согласование', {
'request.id': requestId,
'request.status': status,
'route.approvers': approvers.length,
'route.executors': executors.length,
});
}
}
@@ -345,7 +345,14 @@ export class ApproverService {
const rows = await this.approval.getForRequest(requestId);
const requestStatus = this.computeRequestStatus(rows);
await this.updateRequestStatus(requestId, requestStatus);
await this.logDecision(requestId, rowId, decision, comment, requestStatus);
log.info(DECISION_MESSAGE[decision], {
'request.id': requestId,
'request.list': this.dataListRef,
'request.status': requestStatus,
'approval.decision': decision,
'approval.rowId': rowId,
'approval.comment': comment.slice(0, 500),
});
emitRequestsChanged();
return requestStatus;
}
@@ -374,42 +381,6 @@ export class ApproverService {
return REQUEST_STATUS.execution;
}
/**
* Решение — в логи мониторинга, с кодом и названием заявки, чтобы её было видно прямо в списке логов.
* Код и название дочитываются отдельно; не получилось — пишем без них: решение уже применено.
*/
private async logDecision(
requestId: number,
rowId: number,
decision: ApproverDecision,
comment: string,
requestStatus: string
): Promise<void> {
let code = '';
let title = '';
try {
const item = (await this.dataList().items.getById(requestId).select('Title', 'RequestUniqueCode')()) as {
Title?: string;
RequestUniqueCode?: string;
};
code = item.RequestUniqueCode || '';
title = item.Title || '';
} catch {
// без кода и названия — лог всё равно нужен
}
const what = [code, title].filter(Boolean).join(' · ') || `№${requestId}`;
log.info(`${DECISION_MESSAGE[decision]}: ${what}`, {
'request.id': requestId,
'request.code': code,
'request.title': title,
'request.list': this.dataListRef,
'request.status': requestStatus,
'approval.decision': decision,
'approval.rowId': rowId,
'approval.comment': comment.slice(0, 500),
});
}
private dataList(): IList {
return this.dataListRef.startsWith('/')
? getSP().web.getList(this.dataListRef)
+3 -32
View File
@@ -7,8 +7,6 @@ import { getSP } from './pnp/pnpjs';
import { buildRequestUniqueCode } from './requestCode';
import { dataListTitle } from './listTitles';
import { RequestTypeId } from '../models/types';
import { REQUEST_TYPES } from '../data/requests';
import { isSubmittedStatus } from './approval/models/statuses';
/**
* Список данных, в который фактически пишем: listId (конкретная партиция —
@@ -53,7 +51,6 @@ export async function saveRequestItem(
if (existingId) {
await list.items.getById(existingId).update(item);
logSaved(formId, existingId, undefined, item, false);
return { id: existingId };
}
@@ -62,34 +59,8 @@ export async function saveRequestItem(
// Код известен только после создания элемента (нужен его Id).
const code = buildRequestUniqueCode(codePrefix, id, new Date());
await list.items.getById(id).update({ RequestUniqueCode: code });
logSaved(formId, id, code, item, true);
// Событие для мониторинга: кто (пользователь — из токена) и какую заявку создал. Отправка на согласование
// и решения пишутся отдельно — approvalSubmission.service.ts и approverDecision.service.ts.
log.info('Создана заявка', { 'request.type': formId, 'request.id': id, 'request.code': code });
return { id, code };
}
/**
* События для мониторинга: какая заявка создана и отправлена на согласование (пользователь — из токена).
* В тексте — вид, код и название заявки, чтобы её было видно прямо в списке логов. Решения согласующих
* пишет approverDecision.service.ts.
*/
function logSaved(
formId: RequestTypeId,
id: number,
code: string | undefined,
item: Record<string, unknown>,
created: boolean
): void {
const typeTitle = REQUEST_TYPES.find((t) => t.id === formId)?.title ?? formId;
const title = typeof item.Title === 'string' ? item.Title : '';
const status = typeof item.RequestStatus === 'string' ? item.RequestStatus : '';
const what = [typeTitle, code, title].filter(Boolean).join(' · ');
const attributes = {
'request.type': formId,
'request.typeTitle': typeTitle,
'request.id': id,
'request.code': code ?? '',
'request.title': title,
'request.status': status,
};
if (created) log.info(`Создана заявка: ${what}`, attributes);
if (isSubmittedStatus(status)) log.info(`Заявка отправлена на согласование: ${what}`, attributes);
}