Finalize and Update the processTelemetry helper functions (#1805)
This commit is contained in:
Родитель
eae0d24f4b
Коммит
3e8ed3cede
|
@ -304,7 +304,7 @@ export class Initialization implements IApplicationInsights {
|
|||
// need from core
|
||||
// Microsoft.ApplicationInsights._InternalLogging.throwInternal(
|
||||
// eLoggingSeverity.WARNING,
|
||||
// _InternalMessageId.FailedToSendQueuedTelemetry,
|
||||
// _eInternalMessageId.FailedToSendQueuedTelemetry,
|
||||
// "Failed to send queued telemetry",
|
||||
// properties);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import { SamplingScoreGenerator } from "./SamplingScoreGenerators/SamplingScoreGenerator";
|
||||
import { ISample, Metric } from "@microsoft/applicationinsights-common";
|
||||
import { ITelemetryItem, IDiagnosticLogger, _InternalMessageId, LoggingSeverity, safeGetLogger } from "@microsoft/applicationinsights-core-js";
|
||||
import {
|
||||
IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, eLoggingSeverity, safeGetLogger
|
||||
} from "@microsoft/applicationinsights-core-js";
|
||||
import { SamplingScoreGenerator } from "./SamplingScoreGenerators/SamplingScoreGenerator";
|
||||
|
||||
export class Sample implements ISample {
|
||||
public sampleRate: number;
|
||||
|
@ -16,8 +18,8 @@ export class Sample implements ISample {
|
|||
let _logger = logger || safeGetLogger(null);
|
||||
|
||||
if (sampleRate > 100 || sampleRate < 0) {
|
||||
_logger.throwInternal(LoggingSeverity.WARNING,
|
||||
_InternalMessageId.SampleRateOutOfRange,
|
||||
_logger.throwInternal(eLoggingSeverity.WARNING,
|
||||
_eInternalMessageId.SampleRateOutOfRange,
|
||||
"Sampling rate is out of range (0..100). Sampling will be disabled, you may be sending too much data which may affect your AI service level.",
|
||||
{ samplingRate: sampleRate }, true);
|
||||
sampleRate = 100;
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
import { IConfiguration, ICookieMgrConfig, isNullOrUndefined, ICustomProperties } from "@microsoft/applicationinsights-core-js";
|
||||
import { IConfiguration, isNullOrUndefined, ICustomProperties } from "@microsoft/applicationinsights-core-js";
|
||||
import { DistributedTracingModes } from "../Enums";
|
||||
import { IRequestContext } from "./IRequestContext";
|
||||
|
||||
|
|
|
@ -84,9 +84,9 @@ export interface IConfiguration {
|
|||
* If channels are provided here, core will ignore any channels that are already setup, example if there is a SKU with an initialized channel
|
||||
*/
|
||||
channels?: IChannelControls[][];
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
* @memberof IConfiguration
|
||||
* Flag that disables the Instrumentation Key validation.
|
||||
*/
|
||||
disableInstrumentationKeyValidation?: boolean;
|
||||
|
@ -123,7 +123,6 @@ export interface IConfiguration {
|
|||
* @description Custom cookie domain. This is helpful if you want to share Application Insights cookies across subdomains. It
|
||||
* can be set here or as part of the cookieCfg.domain, the cookieCfg takes precedence if both are specified.
|
||||
* @type {string}
|
||||
* @memberof IConfig
|
||||
* @defaultValue ""
|
||||
*/
|
||||
cookieDomain?: string;
|
||||
|
@ -132,7 +131,6 @@ export interface IConfiguration {
|
|||
* @description Custom cookie path. This is helpful if you want to share Application Insights cookies behind an application
|
||||
* gateway. It can be set here or as part of the cookieCfg.domain, the cookieCfg takes precedence if both are specified.
|
||||
* @type {string}
|
||||
* @memberof IConfig
|
||||
* @defaultValue ""
|
||||
*/
|
||||
cookiePath?: string;
|
||||
|
|
|
@ -20,7 +20,10 @@ import { IPerfManager } from "../JavaScriptSDK.Interfaces/IPerfManager";
|
|||
import { getGblPerfMgr, PerfManager } from "./PerfManager";
|
||||
import { ICookieMgr } from "../JavaScriptSDK.Interfaces/ICookieMgr";
|
||||
import { createCookieMgr } from "./CookieMgr";
|
||||
import { arrForEach, isNullOrUndefined, toISOString, getSetValue, setValue, throwError, isNotTruthy, isFunction, objFreeze, proxyFunctionAs, proxyFunctions } from "./HelperFuncs";
|
||||
import {
|
||||
arrForEach, isNullOrUndefined, getSetValue, setValue, isNotTruthy, isFunction, objExtend, objFreeze, proxyFunctionAs, proxyFunctions, throwError,
|
||||
toISOString
|
||||
} from "./HelperFuncs";
|
||||
import { strExtensionConfig, strIKey } from "./Constants";
|
||||
import { DiagnosticLogger, _InternalLogMessage, _throwInternal, _warnToConsole } from "./DiagnosticLogger";
|
||||
import { getDebugListener } from "./DbgExtensionUtils";
|
||||
|
@ -43,6 +46,11 @@ const strSdkUnloadingError = "SDK is still unloading...";
|
|||
const strSdkNotInitialized = "SDK is not initialized";
|
||||
// const strPluginUnloadFailed = "Failed to unload plugin";
|
||||
|
||||
const defaultInitConfig = {
|
||||
// Have the Diagnostic Logger default to log critical errors to the console
|
||||
loggingLevelConsole: eLoggingSeverity.CRITICAL
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to create the default performance manager
|
||||
* @param core
|
||||
|
@ -361,7 +369,7 @@ export class BaseCore implements IAppInsightsCore {
|
|||
flushComplete: false
|
||||
}
|
||||
|
||||
let processUnloadCtx = createProcessTelemetryUnloadContext(_getPluginChain(), _self.config, _self);
|
||||
let processUnloadCtx = createProcessTelemetryUnloadContext(_getPluginChain(), _self);
|
||||
processUnloadCtx.onComplete(() => {
|
||||
_initDefaults();
|
||||
unloadComplete && unloadComplete(unloadState);
|
||||
|
@ -451,8 +459,8 @@ export class BaseCore implements IAppInsightsCore {
|
|||
_isInitialized = false;
|
||||
|
||||
// Use a default logger so initialization errors are not dropped on the floor with full logging
|
||||
_self.logger = new DiagnosticLogger({ loggingLevelConsole: eLoggingSeverity.CRITICAL });
|
||||
_self.config = {};
|
||||
_self.config = objExtend(true, {}, defaultInitConfig);
|
||||
_self.logger = new DiagnosticLogger(_self.config);
|
||||
_self._extensions = [];
|
||||
|
||||
_telemetryInitializerPlugin = new TelemetryInitializerPlugin();
|
||||
|
@ -601,7 +609,7 @@ export class BaseCore implements IAppInsightsCore {
|
|||
|
||||
if (thePlugins && thePlugins.length > 0) {
|
||||
let unloadChain = createTelemetryProxyChain(thePlugins, _self.config, _self);
|
||||
let unloadCtx = createProcessTelemetryUnloadContext(unloadChain, _self.config, _self);
|
||||
let unloadCtx = createProcessTelemetryUnloadContext(unloadChain, _self);
|
||||
|
||||
unloadCtx.onComplete(() => {
|
||||
let removed = false;
|
||||
|
@ -705,7 +713,7 @@ export class BaseCore implements IAppInsightsCore {
|
|||
}
|
||||
|
||||
function _doUpdate(updateState: ITelemetryUpdateState): void {
|
||||
let updateCtx = createProcessTelemetryUpdateContext(_getPluginChain(), _self.config, _self);
|
||||
let updateCtx = createProcessTelemetryUpdateContext(_getPluginChain(), _self);
|
||||
|
||||
if (!_self._updateHook || _self._updateHook(updateCtx, updateState) !== true) {
|
||||
updateCtx.processNext(updateState);
|
||||
|
|
|
@ -120,14 +120,15 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
|
|||
_self.teardown = (unloadCtx?: IProcessTelemetryUnloadContext, unloadState?: ITelemetryUnloadState) => {
|
||||
// If this plugin has already been torn down (not operational) or is not initialized (core is not set)
|
||||
// or the core being used for unload was not the same core used for initialization.
|
||||
if (!_self.core || (unloadCtx && _self.core !== unloadCtx.core())) {
|
||||
let core = _self.core;
|
||||
if (!core || (unloadCtx && core !== unloadCtx.core())) {
|
||||
// Do Nothing as either the plugin is not initialized or was not initialized by the current core
|
||||
return;
|
||||
}
|
||||
|
||||
let result: void | boolean;
|
||||
let unloadDone = false;
|
||||
let theUnloadCtx = unloadCtx || createProcessTelemetryUnloadContext(null, {}, _self.core, _nextPlugin && _nextPlugin[strGetPlugin] ? _nextPlugin[strGetPlugin]() : _nextPlugin);
|
||||
let theUnloadCtx = unloadCtx || createProcessTelemetryUnloadContext(null, core, _nextPlugin && _nextPlugin[strGetPlugin] ? _nextPlugin[strGetPlugin]() : _nextPlugin);
|
||||
let theUnloadState: ITelemetryUnloadState = unloadState || {
|
||||
reason: TelemetryUnloadReason.ManualTeardown,
|
||||
isAsync: false
|
||||
|
@ -166,14 +167,15 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
|
|||
_self.update = (updateCtx: IProcessTelemetryUpdateContext, updateState: ITelemetryUpdateState) => {
|
||||
// If this plugin has already been torn down (not operational) or is not initialized (core is not set)
|
||||
// or the core being used for unload was not the same core used for initialization.
|
||||
if (!_self.core || (updateCtx && _self.core !== updateCtx.core())) {
|
||||
let core = _self.core;
|
||||
if (!core || (updateCtx && core !== updateCtx.core())) {
|
||||
// Do Nothing
|
||||
return;
|
||||
}
|
||||
|
||||
let result: void | boolean;
|
||||
let updateDone = false;
|
||||
let theUpdateCtx = updateCtx || createProcessTelemetryUpdateContext(null, {}, _self.core, _nextPlugin && _nextPlugin[strGetPlugin] ? _nextPlugin[strGetPlugin]() : _nextPlugin);
|
||||
let theUpdateCtx = updateCtx || createProcessTelemetryUpdateContext(null, core, _nextPlugin && _nextPlugin[strGetPlugin] ? _nextPlugin[strGetPlugin]() : _nextPlugin);
|
||||
let theUpdateState: ITelemetryUpdateState = updateState || {
|
||||
reason: TelemetryUpdateReason.Unknown
|
||||
};
|
||||
|
|
|
@ -43,7 +43,7 @@ interface IInternalContext<T extends IBaseProcessingContext> {
|
|||
ctx: T
|
||||
}
|
||||
|
||||
function _getNextProxyStart(proxy: ITelemetryPluginChain, config: IConfiguration, core:IAppInsightsCore, startAt: IPlugin): ITelemetryPluginChain {
|
||||
function _getNextProxyStart<T, C = IConfiguration>(proxy: ITelemetryPluginChain, core: IAppInsightsCore, startAt: IPlugin): ITelemetryPluginChain {
|
||||
while (proxy) {
|
||||
if (proxy.getPlugin() === startAt) {
|
||||
return proxy;
|
||||
|
@ -53,7 +53,7 @@ function _getNextProxyStart(proxy: ITelemetryPluginChain, config: IConfiguration
|
|||
}
|
||||
|
||||
// This wasn't found in the existing chain so create an isolated one with just this plugin
|
||||
return createTelemetryProxyChain([startAt], config, core);
|
||||
return createTelemetryProxyChain([startAt], core.config || {}, core);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +64,7 @@ function _getNextProxyStart(proxy: ITelemetryPluginChain, config: IConfiguration
|
|||
* @param startAt - Identifies the next plugin to execute, if null there is no "next" plugin and if undefined it should assume the start of the chain
|
||||
* @returns
|
||||
*/
|
||||
function _createInternalContext<T extends IBaseProcessingContext>(telemetryChain: ITelemetryPluginChain, config: IConfiguration, core:IAppInsightsCore, startAt?: IPlugin): IInternalContext<T> {
|
||||
function _createInternalContext<T extends IBaseProcessingContext>(telemetryChain: ITelemetryPluginChain, config: IConfiguration, core: IAppInsightsCore, startAt?: IPlugin): IInternalContext<T> {
|
||||
// We have a special case where we want to start execution from this specific plugin
|
||||
// or we simply reuse the existing telemetry plugin chain (normal execution case)
|
||||
let _nextProxy: ITelemetryPluginChain | null = null; // By Default set as no next plugin
|
||||
|
@ -72,7 +72,7 @@ function _createInternalContext<T extends IBaseProcessingContext>(telemetryChain
|
|||
|
||||
if (startAt !== null) {
|
||||
// There is no next element (null) vs not defined (undefined) so use the full chain
|
||||
_nextProxy = startAt ? _getNextProxyStart(telemetryChain, config, core, startAt) : telemetryChain;
|
||||
_nextProxy = startAt ? _getNextProxyStart(telemetryChain, core, startAt) : telemetryChain;
|
||||
}
|
||||
|
||||
let context: IInternalContext<T> = {
|
||||
|
@ -159,7 +159,7 @@ function _createInternalContext<T extends IBaseProcessingContext>(telemetryChain
|
|||
let newConfig = objExtend(true, defaultValue, theConfig);
|
||||
|
||||
if (config && mergeDefault === GetExtCfgMergeType.MergeDefaultFromRootOrDefault) {
|
||||
// Enumerate over the defaultValues and if not already populate attempt to
|
||||
// Enumerate over the defaultValues and if not already populated attempt to
|
||||
// find a value from the root config
|
||||
objForEachKey(defaultValue, (field) => {
|
||||
// for each unspecified field, set the default value
|
||||
|
@ -171,6 +171,8 @@ function _createInternalContext<T extends IBaseProcessingContext>(telemetryChain
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
theConfig = newConfig;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,7 +246,8 @@ export function createProcessTelemetryContext(telemetryChain: ITelemetryPluginCh
|
|||
* @param core - The current core instance
|
||||
* @param startAt - Identifies the next plugin to execute, if null there is no "next" plugin and if undefined it should assume the start of the chain
|
||||
*/
|
||||
export function createProcessTelemetryUnloadContext(telemetryChain: ITelemetryPluginChain, config: IConfiguration, core:IAppInsightsCore, startAt?: IPlugin): IProcessTelemetryUnloadContext {
|
||||
export function createProcessTelemetryUnloadContext(telemetryChain: ITelemetryPluginChain, core: IAppInsightsCore, startAt?: IPlugin): IProcessTelemetryUnloadContext {
|
||||
let config = core.config || {};
|
||||
let internalContext: IInternalContext<IProcessTelemetryUnloadContext> = _createInternalContext<IProcessTelemetryUnloadContext>(telemetryChain, config, core, startAt);
|
||||
let context = internalContext.ctx;
|
||||
|
||||
|
@ -260,7 +263,7 @@ export function createProcessTelemetryUnloadContext(telemetryChain: ITelemetryPl
|
|||
plugins = createTelemetryProxyChain(plugins, config, core, startAt);
|
||||
}
|
||||
|
||||
return createProcessTelemetryUnloadContext(plugins || context.getNext(), config, core, startAt);
|
||||
return createProcessTelemetryUnloadContext(plugins || context.getNext(), core, startAt);
|
||||
}
|
||||
|
||||
context.processNext = _processNext;
|
||||
|
@ -276,14 +279,15 @@ export function createProcessTelemetryUnloadContext(telemetryChain: ITelemetryPl
|
|||
* @param core - The current core instance
|
||||
* @param startAt - Identifies the next plugin to execute, if null there is no "next" plugin and if undefined it should assume the start of the chain
|
||||
*/
|
||||
export function createProcessTelemetryUpdateContext(telemetryChain: ITelemetryPluginChain, config: IConfiguration, core:IAppInsightsCore, startAt?: IPlugin): IProcessTelemetryUpdateContext {
|
||||
export function createProcessTelemetryUpdateContext(telemetryChain: ITelemetryPluginChain, core: IAppInsightsCore, startAt?: IPlugin): IProcessTelemetryUpdateContext {
|
||||
let config = core.config || {};
|
||||
let internalContext: IInternalContext<IProcessTelemetryUpdateContext> = _createInternalContext<IProcessTelemetryUpdateContext>(telemetryChain, config, core, startAt);
|
||||
let context = internalContext.ctx;
|
||||
|
||||
function _processNext(updateState: ITelemetryUpdateState) {
|
||||
return context.iterate((plugin) => {
|
||||
if (isFunction(plugin[strUpdate])) {
|
||||
plugin[strUpdate](context, updateState);
|
||||
if (isFunction(plugin.update)) {
|
||||
plugin.update(context, updateState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -293,7 +297,7 @@ export function createProcessTelemetryUpdateContext(telemetryChain: ITelemetryPl
|
|||
plugins = createTelemetryProxyChain(plugins, config, core, startAt);
|
||||
}
|
||||
|
||||
return createProcessTelemetryUpdateContext(plugins || context.getNext(), config, core, startAt);
|
||||
return createProcessTelemetryUpdateContext(plugins || context.getNext(), core, startAt);
|
||||
}
|
||||
|
||||
context.processNext = _processNext;
|
||||
|
@ -446,7 +450,7 @@ export function createTelemetryPluginProxy(plugin: ITelemetryPlugin, config: ICo
|
|||
itemCtx.diagLog(),
|
||||
eLoggingSeverity.CRITICAL,
|
||||
_eInternalMessageId.PluginException,
|
||||
"Plugin [" + plugin.identifier + "] failed during " + name + " - " + dumpObj(error) + ", run flags: " + dumpObj(hasRunContext));
|
||||
"Plugin [" + identifier + "] failed during " + name + " - " + dumpObj(error) + ", run flags: " + dumpObj(hasRunContext));
|
||||
}
|
||||
}
|
||||
}, details, isAsync);
|
||||
|
@ -464,7 +468,7 @@ export function createTelemetryPluginProxy(plugin: ITelemetryPlugin, config: ICo
|
|||
}
|
||||
|
||||
let pluginState = _getPluginState(plugin);
|
||||
if (pluginState[strTeardown] || pluginState[strDisabled]) {
|
||||
if (pluginState.teardown || pluginState[strDisabled]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -627,7 +631,7 @@ export class ProcessTelemetryContext implements IProcessTelemetryContext {
|
|||
* @param config - The current config
|
||||
* @param core - The current core instance
|
||||
*/
|
||||
constructor(pluginChain: ITelemetryPluginChain, config: IConfiguration, core:IAppInsightsCore, startAt?:IPlugin) {
|
||||
constructor(pluginChain: ITelemetryPluginChain, config: IConfiguration, core: IAppInsightsCore, startAt?:IPlugin) {
|
||||
let _self = this;
|
||||
|
||||
let context = createProcessTelemetryContext(pluginChain, config, core, startAt);
|
||||
|
|
|
@ -16,7 +16,7 @@ const strDoUnload = "_doUnload";
|
|||
export interface IPluginState {
|
||||
core?: IAppInsightsCore;
|
||||
isInitialized?: boolean;
|
||||
tearDown?: boolean;
|
||||
teardown?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ function showHelp() {
|
|||
console.log(" -patch - Increment the current version to the next patch number (x.y.z => x.y.[z+1]");
|
||||
console.log(" -minor - Increment the current version to the next minor number (x.y.z => x.[y+1].0");
|
||||
console.log(" -major - Increment the current version to the next major number (x.y.z => [x+1].0.0");
|
||||
console.log(" -next - Increment the current version to the next value (patch, minor or major) based on the 'next' value in version.json");
|
||||
console.log(" -dev - Add the 'dev' pre-release to the number (x.y.z => x.y.z-dev)");
|
||||
console.log(" -alpha - Add the 'alpha' pre-release to the number (x.y.z => x.y.z-alpha)");
|
||||
console.log(" -beta - Add the 'beta' pre-release to the number (x.y.z => x.y.z-beta)");
|
||||
|
@ -166,7 +167,7 @@ function parseArgs() {
|
|||
|
||||
function updateVersions() {
|
||||
// Get the configured next release, default to "patch"
|
||||
const verNext = theVersion.next || "patch";
|
||||
const verNext = theVersion.next = theVersion.next || "patch";
|
||||
const rootVersion = require(process.cwd() + "/package.json");
|
||||
let newVersion = calculateVersion(rootVersion.version, verNext);
|
||||
if (newVersion) {
|
||||
|
|
|
@ -13,8 +13,23 @@ export function importCheck(options:IImportCheckRollupOptions = {}) {
|
|||
keywords: []
|
||||
};
|
||||
|
||||
// Don't allow importing from folders from a package
|
||||
checkOptions.keywords.push({
|
||||
funcNames: [ /import.*@microsoft\/[a-z\-]+\//gi ],
|
||||
errorMsg: "Importing this module has been blocked, you should be importing directly from the root of the package and not from a deployed file of the package - [%funcName%]",
|
||||
errorTitle: "Invalid Import detected"
|
||||
});
|
||||
|
||||
// Check enum map lookups to ensure they are used correctly
|
||||
checkOptions.keywords.push({
|
||||
funcNames: [ /(\w[\d\w]*)\[\1\.(\w[\w\d]*)\]/g ],
|
||||
errorMsg: "Incorrect usage of an indexed map lookup detected - [%funcName%] you should use the enum name value as the lookup not the map name -- eg. Name[eName.xxxx]",
|
||||
errorTitle: "Incorrect usage of indexed map lookup"
|
||||
});
|
||||
|
||||
|
||||
for (let lp = 0; lp < ((options.exclude)||[]).length; lp++) {
|
||||
if (options.exclude && checkOptions.keywords) {
|
||||
if (options.exclude) {
|
||||
checkOptions.keywords.push({
|
||||
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||
funcNames: [ new RegExp("import[\\s]*(\\*|\\{[^\\}]*\\})[\\s]*from[\\s]*[\\'\\\"][^\\'\\\"]*" + _escapeRegEx(options.exclude[lp]) + "[\\'\\\"]", "gi") ],
|
||||
|
|
|
@ -28,8 +28,16 @@ export function checkResult(tokens:IEs3CheckKeyword[], result:string, id:string,
|
|||
while ((funcMatch = funcRegEx.exec(result))) {
|
||||
let funcName = funcMatch[0]||"";
|
||||
if (funcName.length > 0 && !isIgnoreFuncMatch(funcName, keyword)) {
|
||||
errorMessage += formatError(keyword, funcName, keyword.errorMsg, result, funcMatch.index, id, entry);
|
||||
errorMessage += "\n--------------------=([" + visibleNewlines(funcName) + "])=--------------------\n";
|
||||
let newErrorMessage = formatError(keyword, funcName, keyword.errorMsg, result, funcMatch.index, id, entry);
|
||||
if ((errorMessage.length + newErrorMessage.length) < 32768) {
|
||||
errorMessage += formatError(keyword, funcName, keyword.errorMsg, result, funcMatch.index, id, entry);
|
||||
errorMessage += "\n--------------------=([" + visibleNewlines(funcName) + "])=--------------------\n";
|
||||
} else {
|
||||
errorMessage += "\n-------------------------------------------------------------------------------";
|
||||
errorMessage += "\n Too Many errors detected!";
|
||||
errorMessage += "\n-------------------------------------------------------------------------------";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче