issue-sticker 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +22 -5
- package/dist/{createLucideIcon-CmSNiW63.js → createLucideIcon-BzRMP02h.js} +102 -99
- package/dist/{externalUserPayload-hJjuUx6q.js → externalUserPayload-zu5IXBjn.js} +10101 -8502
- package/dist/issue-sticker-headless-react.js +129 -72
- package/dist/issue-sticker-headless.js +219 -91
- package/dist/issue-sticker-react.js +4480 -3219
- package/dist/{tooltip-DciWyUWs.js → tooltip-B6UPO-U8.js} +1 -1
- package/headless.d.ts +31 -4
- package/package.json +3 -2
- package/react.d.ts +9 -1
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, n as normalizeExposedJiraFields, b as normalizeJiraFieldId, g as getElementPosition, c as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, h as buildGuestOwnerQuery,
|
|
1
|
+
import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadToSignedUrl, p as pristineFetch, n as normalizeExposedJiraFields, b as normalizeJiraFieldId, g as getElementPosition, c as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, h as buildGuestOwnerQuery, i as buildIssueIntegrationsPayload, j as devLogger, C as ConsoleCollector, N as NetworkCollector, s as selectNetworkLogsForSubmit, k as filterExcludeServiceRelatedLogs, l as filterExcludeServiceRelatedConsoleLogs, m as compactNetworkLogsForTransport, o as CONSOLE_LOG_SUBMIT_LIMIT, q as buildExternalUserPayload, r as shrinkNetworkLogsToFit, t as measureJsonBytes, v as sanitizeString, w as getCurrentPageUrl, S as ScreenRecordingController, x as SessionReplayCaptureController, y as preloadModernScreenshot, I as ISSUE_STICKER_INTERNAL_LOG_PREFIX, z as isIssueStickerExtensionRuntimeActive } from "./externalUserPayload-zu5IXBjn.js";
|
|
2
2
|
import "react/jsx-runtime";
|
|
3
3
|
import "react";
|
|
4
|
-
import "./createLucideIcon-
|
|
4
|
+
import "./createLucideIcon-BzRMP02h.js";
|
|
5
|
+
const WINDOW_MS = 6e4;
|
|
6
|
+
const MAX_PER_WINDOW = 5;
|
|
7
|
+
class AutoIssueRateLimit {
|
|
8
|
+
constructor(max = MAX_PER_WINDOW, windowMs = WINDOW_MS) {
|
|
9
|
+
this.max = max;
|
|
10
|
+
this.windowMs = windowMs;
|
|
11
|
+
this.accepted = [];
|
|
12
|
+
}
|
|
13
|
+
get label() {
|
|
14
|
+
return `${this.windowMs / 1e3}초에 ${this.max}건`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 자리가 있으면 차지하고 true 를 돌려준다. 실패한 호출도 서버 왕복을 이미 썼으므로
|
|
18
|
+
* 자리를 돌려주지 않는다.
|
|
19
|
+
*/
|
|
20
|
+
claim(now = Date.now()) {
|
|
21
|
+
this.accepted = this.accepted.filter((at) => now - at < this.windowMs);
|
|
22
|
+
if (this.accepted.length >= this.max) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
this.accepted.push(now);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
reset() {
|
|
29
|
+
this.accepted = [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
5
32
|
class IssueStickerHeadlessError extends Error {
|
|
6
33
|
constructor(code, message, cause) {
|
|
7
34
|
super(message);
|
|
@@ -68,7 +95,7 @@ class HeadlessTransport {
|
|
|
68
95
|
const authHeader = await this.options.resolveAuthorizationHeader();
|
|
69
96
|
if (authHeader) headers.Authorization = authHeader;
|
|
70
97
|
}
|
|
71
|
-
const execute = (nextHeaders) =>
|
|
98
|
+
const execute = (nextHeaders) => pristineFetch(`${getApiBaseUrl()}${endpoint}`, {
|
|
72
99
|
method: options.method ?? "GET",
|
|
73
100
|
headers: nextHeaders,
|
|
74
101
|
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
@@ -604,7 +631,7 @@ function composeIssueFilterGroups(integrations, customFields) {
|
|
|
604
631
|
function buildHeadlessCaptureContext({
|
|
605
632
|
target,
|
|
606
633
|
event,
|
|
607
|
-
videoFile,
|
|
634
|
+
videoFile = null,
|
|
608
635
|
createVideoPreviewUrl,
|
|
609
636
|
sessionReplayEnabled
|
|
610
637
|
}) {
|
|
@@ -614,7 +641,7 @@ function buildHeadlessCaptureContext({
|
|
|
614
641
|
mediaMode,
|
|
615
642
|
position,
|
|
616
643
|
screenshotPreviewUrls: [],
|
|
617
|
-
videoPreviewUrl: mediaMode === "video" && videoFile ? createVideoPreviewUrl(videoFile) : null,
|
|
644
|
+
videoPreviewUrl: mediaMode === "video" && videoFile && createVideoPreviewUrl ? createVideoPreviewUrl(videoFile) : null,
|
|
618
645
|
targetSelector: position.selector,
|
|
619
646
|
sessionReplayEnabled,
|
|
620
647
|
sessionReplayPreview: null
|
|
@@ -624,6 +651,7 @@ async function captureHeadlessScreenshots({
|
|
|
624
651
|
target,
|
|
625
652
|
excludeSelectors
|
|
626
653
|
}) {
|
|
654
|
+
const targetIsDocumentRoot = target === document.body || target === document.documentElement;
|
|
627
655
|
const screenshots = await Promise.all([
|
|
628
656
|
captureWithModernScreenshot({
|
|
629
657
|
element: document.documentElement,
|
|
@@ -631,10 +659,12 @@ async function captureHeadlessScreenshots({
|
|
|
631
659
|
width: window.innerWidth,
|
|
632
660
|
height: window.innerHeight
|
|
633
661
|
}).catch(() => void 0),
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
662
|
+
...targetIsDocumentRoot ? [] : [
|
|
663
|
+
captureWithModernScreenshot({
|
|
664
|
+
element: target,
|
|
665
|
+
excludeSelectors
|
|
666
|
+
}).catch(() => void 0)
|
|
667
|
+
]
|
|
638
668
|
]);
|
|
639
669
|
return screenshots.filter(
|
|
640
670
|
(dataUrl) => typeof dataUrl === "string" && dataUrl.length > 0
|
|
@@ -827,9 +857,7 @@ class HeadlessIssueRepository {
|
|
|
827
857
|
this.options.getAuthMode(),
|
|
828
858
|
this.options.getDefaultExternalUserId()
|
|
829
859
|
);
|
|
830
|
-
return this.request(
|
|
831
|
-
`/api/sdk/issues/${encodeURIComponent(issueId)}${ownerQuery}`
|
|
832
|
-
);
|
|
860
|
+
return this.request(`/api/sdk/issues/${encodeURIComponent(issueId)}${ownerQuery}`);
|
|
833
861
|
}
|
|
834
862
|
createIssue(issueData) {
|
|
835
863
|
return this.request("/api/sdk/issues", {
|
|
@@ -865,73 +893,60 @@ class HeadlessIssueRepository {
|
|
|
865
893
|
return Array.isArray(fields) ? fields : [];
|
|
866
894
|
}
|
|
867
895
|
/**
|
|
868
|
-
* 이슈
|
|
896
|
+
* Jira/Notion 외부 이슈 생성 페이로드를 만든다. 서버가 이슈 생성과 같은 요청
|
|
897
|
+
* 안에서 외부 연동을 수행하므로, 생성 요청(createIssue)에 실어 보낸다.
|
|
898
|
+
*
|
|
899
|
+
* 판정 규칙은 기존과 동일 — getActiveIntegration() 이 돌려주는 활성 연동의
|
|
900
|
+
* service_type 기준(jira 는 `active?.service_type === 'JIRA'`, notion 은
|
|
901
|
+
* `active?.service_type === 'NOTION'`). 담을 게 없으면 undefined 를 돌려줘
|
|
902
|
+
* 호출자가 `integrations` 키 자체를 빼게 한다.
|
|
903
|
+
*/
|
|
904
|
+
async buildIntegrationsPayload(draft, schema) {
|
|
905
|
+
if (this.options.getAuthMode() !== "member") {
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
const active = await this.getActiveIntegration().catch(() => null);
|
|
909
|
+
const fields = (schema == null ? void 0 : schema.fields) ?? [];
|
|
910
|
+
return buildIssueIntegrationsPayload({
|
|
911
|
+
jiraEnabled: (active == null ? void 0 : active.service_type) === "JIRA",
|
|
912
|
+
jiraFields: getFilledDynamicValues(draft, fields, "jira"),
|
|
913
|
+
notionEnabled: (active == null ? void 0 : active.service_type) === "NOTION",
|
|
914
|
+
notionProperties: getFilledDynamicValues(draft, fields, "notion")
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* 이슈 저장 이후 커스텀 필드 값을 반영한다. Jira/Notion 외부 이슈 생성은
|
|
919
|
+
* buildIntegrationsPayload 를 통해 생성 요청 안에서 서버가 수행하므로 여기서는
|
|
920
|
+
* 다루지 않는다.
|
|
869
921
|
*
|
|
870
922
|
* 실패는 이슈 저장을 되돌리지 않으므로 throw 하지 않고 결과로 돌려준다. headless 는
|
|
871
923
|
* UI 가 없어 toast 를 띄울 수 없으니, 통합하는 쪽이 이 값을 보고 판단해야 한다.
|
|
872
924
|
*/
|
|
873
|
-
async syncDynamicFields(createdIssue, draft, schema
|
|
925
|
+
async syncDynamicFields(createdIssue, draft, schema) {
|
|
874
926
|
if (this.options.getAuthMode() !== "member") {
|
|
875
927
|
return { failedServices: [] };
|
|
876
928
|
}
|
|
877
|
-
const active = await this.getActiveIntegration().catch(() => null);
|
|
878
929
|
const fields = (schema == null ? void 0 : schema.fields) ?? [];
|
|
879
|
-
const jiraValues = getFilledDynamicValues(draft, fields, "jira");
|
|
880
|
-
const notionValues = getFilledDynamicValues(draft, fields, "notion");
|
|
881
930
|
const customValues = getFilledDynamicValues(draft, fields, "custom");
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
createIssue: (input) => this.request("/api/jira/issues", {
|
|
893
|
-
method: "POST",
|
|
894
|
-
body: {
|
|
895
|
-
projectId: this.options.getProjectId(),
|
|
896
|
-
issueId: input.issueId,
|
|
897
|
-
issueData: {
|
|
898
|
-
title: input.title,
|
|
899
|
-
description: input.description,
|
|
900
|
-
url: input.url,
|
|
901
|
-
screenshotUrl: input.screenshotUrl,
|
|
902
|
-
videoUrl: input.videoUrl,
|
|
903
|
-
fields: input.fields
|
|
931
|
+
let hasCustomFieldFailure = false;
|
|
932
|
+
await Promise.all(
|
|
933
|
+
Object.entries(customValues).map(
|
|
934
|
+
([fieldDefinitionId, value]) => this.request(
|
|
935
|
+
`/api/sdk/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
|
|
936
|
+
{
|
|
937
|
+
method: "PUT",
|
|
938
|
+
body: {
|
|
939
|
+
field_definition_id: fieldDefinitionId,
|
|
940
|
+
value
|
|
904
941
|
}
|
|
905
942
|
}
|
|
943
|
+
).catch((error) => {
|
|
944
|
+
hasCustomFieldFailure = true;
|
|
945
|
+
devLogger.error("headless", `custom-field 연동 실패 (이슈 저장은 성공):`, error);
|
|
906
946
|
})
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
properties: notionValues,
|
|
911
|
-
createPage: (input) => this.request("/api/notion/pages", {
|
|
912
|
-
method: "POST",
|
|
913
|
-
body: {
|
|
914
|
-
projectId: this.options.getProjectId(),
|
|
915
|
-
issueId: input.issueId,
|
|
916
|
-
properties: input.properties
|
|
917
|
-
}
|
|
918
|
-
})
|
|
919
|
-
},
|
|
920
|
-
customFieldValues: customValues,
|
|
921
|
-
upsertCustomFieldValue: (fieldDefinitionId, value) => this.request(
|
|
922
|
-
`/api/sdk/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
|
|
923
|
-
{
|
|
924
|
-
method: "PUT",
|
|
925
|
-
body: {
|
|
926
|
-
field_definition_id: fieldDefinitionId,
|
|
927
|
-
value
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
),
|
|
931
|
-
onError: (service, error) => {
|
|
932
|
-
devLogger.error("headless", `${service} 연동 실패 (이슈 저장은 성공):`, error);
|
|
933
|
-
}
|
|
934
|
-
});
|
|
947
|
+
)
|
|
948
|
+
);
|
|
949
|
+
return { failedServices: hasCustomFieldFailure ? ["custom-field"] : [] };
|
|
935
950
|
}
|
|
936
951
|
async fetchAllIssueStickers(filters) {
|
|
937
952
|
var _a, _b, _c;
|
|
@@ -1043,14 +1058,24 @@ class HeadlessLogCollectors {
|
|
|
1043
1058
|
this.networkCollector = null;
|
|
1044
1059
|
this.consoleCollector = null;
|
|
1045
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* 스크린샷 캡처처럼 **우리가 서드파티 요청을 일으키는** 구간을 감싼다.
|
|
1063
|
+
*
|
|
1064
|
+
* 감싸지 않으면 스크린샷 라이브러리가 인라인하려고 끌어오는 웹폰트·이미지가 최근 50건을
|
|
1065
|
+
* 통째로 채워, 정작 필요한 앱 요청이 링버퍼 밖으로 밀려난다.
|
|
1066
|
+
*/
|
|
1067
|
+
async pauseNetworkWhile(task) {
|
|
1068
|
+
const collector = this.networkCollector;
|
|
1069
|
+
return (collector == null ? void 0 : collector.pauseWhile) ? collector.pauseWhile(task) : task();
|
|
1070
|
+
}
|
|
1046
1071
|
collect() {
|
|
1047
1072
|
var _a, _b;
|
|
1048
|
-
const networkLogs =
|
|
1049
|
-
((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? []
|
|
1050
|
-
)
|
|
1073
|
+
const networkLogs = selectNetworkLogsForSubmit(
|
|
1074
|
+
filterExcludeServiceRelatedLogs(((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? [])
|
|
1075
|
+
);
|
|
1051
1076
|
const consoleLogs = filterExcludeServiceRelatedConsoleLogs(
|
|
1052
1077
|
((_b = this.consoleCollector) == null ? void 0 : _b.getFilteredLogs()) ?? []
|
|
1053
|
-
).slice(-
|
|
1078
|
+
).slice(-CONSOLE_LOG_SUBMIT_LIMIT);
|
|
1054
1079
|
return {
|
|
1055
1080
|
networkLogs: compactNetworkLogsForTransport(networkLogs) ?? [],
|
|
1056
1081
|
consoleLogs
|
|
@@ -1106,6 +1131,7 @@ function createSessionReplayMeta(snapshot) {
|
|
|
1106
1131
|
}
|
|
1107
1132
|
function createInitialHeadlessSnapshot() {
|
|
1108
1133
|
return {
|
|
1134
|
+
extensionActive: false,
|
|
1109
1135
|
phase: "idle",
|
|
1110
1136
|
auth: {
|
|
1111
1137
|
mode: "public",
|
|
@@ -1242,10 +1268,15 @@ function buildIssueCreatePayload({
|
|
|
1242
1268
|
currentUrl,
|
|
1243
1269
|
userAgent
|
|
1244
1270
|
}) {
|
|
1245
|
-
|
|
1271
|
+
var _a, _b, _c;
|
|
1272
|
+
const title = (_a = draft.title) == null ? void 0 : _a.trim();
|
|
1273
|
+
const description = (_b = draft.description) == null ? void 0 : _b.trim();
|
|
1274
|
+
const intent = (_c = draft.intent) == null ? void 0 : _c.trim();
|
|
1275
|
+
const base = {
|
|
1246
1276
|
url: currentUrl,
|
|
1247
|
-
title:
|
|
1248
|
-
description:
|
|
1277
|
+
...title ? { title } : {},
|
|
1278
|
+
...description ? { description } : {},
|
|
1279
|
+
...intent ? { intent } : {},
|
|
1249
1280
|
position: draft.position,
|
|
1250
1281
|
...uploads.screenshotUploadIds.length > 0 ? { screenshot_upload_ids: uploads.screenshotUploadIds } : {},
|
|
1251
1282
|
...uploads.videoUpload ? { video_upload_id: uploads.videoUpload.id } : {},
|
|
@@ -1253,14 +1284,24 @@ function buildIssueCreatePayload({
|
|
|
1253
1284
|
session_replay_upload_id: uploads.replayUpload.uploadId,
|
|
1254
1285
|
session_replay_meta: uploads.replayUpload.meta
|
|
1255
1286
|
} : {},
|
|
1256
|
-
network_logs: logs.networkLogs.length > 0 ? logs.networkLogs : void 0,
|
|
1257
1287
|
console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
|
|
1258
1288
|
user_agent: userAgent,
|
|
1259
1289
|
tenant_id: auth.tenantId,
|
|
1260
1290
|
project_id: projectId,
|
|
1261
1291
|
account_id: auth.accountId ?? void 0,
|
|
1262
1292
|
...buildExternalUserPayload(config.user)
|
|
1263
|
-
}
|
|
1293
|
+
};
|
|
1294
|
+
const { networkLogs, stillOverBudget } = shrinkNetworkLogsToFit(
|
|
1295
|
+
logs.networkLogs,
|
|
1296
|
+
measureJsonBytes(base)
|
|
1297
|
+
);
|
|
1298
|
+
return {
|
|
1299
|
+
payload: sanitizeString({
|
|
1300
|
+
...base,
|
|
1301
|
+
network_logs: networkLogs.length > 0 ? networkLogs : void 0
|
|
1302
|
+
}),
|
|
1303
|
+
stillOverBudget
|
|
1304
|
+
};
|
|
1264
1305
|
}
|
|
1265
1306
|
async function submitHeadlessIssueDraft({
|
|
1266
1307
|
draft,
|
|
@@ -1276,12 +1317,11 @@ async function submitHeadlessIssueDraft({
|
|
|
1276
1317
|
userAgent,
|
|
1277
1318
|
uploadSessionReplaySnapshot
|
|
1278
1319
|
}) {
|
|
1279
|
-
var _a;
|
|
1280
1320
|
const screenshotUploads = draft.mediaMode === "screenshot" ? await transport.uploadScreenshotPreviews(draft.screenshotPreviewUrls) : [];
|
|
1281
1321
|
const screenshotUploadIds = screenshotUploads.map((upload) => upload.id);
|
|
1282
1322
|
const videoUpload = draft.mediaMode === "video" && pendingVideoFile ? await transport.uploadSignedMedia(pendingVideoFile, "video") : null;
|
|
1283
1323
|
const replayUpload = draft.sessionReplayEnabled ? await uploadSessionReplaySnapshot() : null;
|
|
1284
|
-
const issueData = buildIssueCreatePayload({
|
|
1324
|
+
const { payload: issueData, stillOverBudget } = buildIssueCreatePayload({
|
|
1285
1325
|
draft,
|
|
1286
1326
|
auth,
|
|
1287
1327
|
config,
|
|
@@ -1291,11 +1331,15 @@ async function submitHeadlessIssueDraft({
|
|
|
1291
1331
|
currentUrl,
|
|
1292
1332
|
userAgent
|
|
1293
1333
|
});
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1334
|
+
if (stillOverBudget) {
|
|
1335
|
+
throw new IssueStickerHeadlessError(
|
|
1336
|
+
"PAYLOAD_TOO_LARGE",
|
|
1337
|
+
"이슈 데이터가 전송 한도를 넘어 저장할 수 없습니다. 설명을 줄이거나 첨부를 빼고 다시 시도해주세요."
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const integrations = await issueRepository.buildIntegrationsPayload(draft, formSchema);
|
|
1341
|
+
const createdIssue = await issueRepository.createIssue({ ...issueData, integrations });
|
|
1342
|
+
await issueRepository.syncDynamicFields(createdIssue, draft, formSchema);
|
|
1299
1343
|
return createdIssue;
|
|
1300
1344
|
}
|
|
1301
1345
|
const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
|
|
@@ -1393,6 +1437,7 @@ class HeadlessClientImpl {
|
|
|
1393
1437
|
var _a;
|
|
1394
1438
|
this.config = config;
|
|
1395
1439
|
this.state = new HeadlessClientState();
|
|
1440
|
+
this.autoIssueRateLimit = new AutoIssueRateLimit();
|
|
1396
1441
|
this.pendingMedia = new HeadlessPendingMedia();
|
|
1397
1442
|
this.initializePromise = null;
|
|
1398
1443
|
this.captureCleanup = null;
|
|
@@ -1419,7 +1464,7 @@ class HeadlessClientImpl {
|
|
|
1419
1464
|
var _a2;
|
|
1420
1465
|
return (_a2 = this.config.user) == null ? void 0 : _a2.id;
|
|
1421
1466
|
},
|
|
1422
|
-
getCurrentUrl: () =>
|
|
1467
|
+
getCurrentUrl: () => getCurrentPageUrl() || void 0
|
|
1423
1468
|
});
|
|
1424
1469
|
this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
|
|
1425
1470
|
this.screenRecordingController = new ScreenRecordingController({
|
|
@@ -1596,14 +1641,15 @@ class HeadlessClientImpl {
|
|
|
1596
1641
|
this.state.setSnapshot({ phase: "idle", draft: null, formSchema: null, error: null });
|
|
1597
1642
|
}
|
|
1598
1643
|
async submitDraft(input) {
|
|
1644
|
+
var _a, _b, _c;
|
|
1599
1645
|
await this.ensureInitialized();
|
|
1600
1646
|
this.state.assertPhase(["draftingIssue"], "submitDraft");
|
|
1601
1647
|
this.state.requireDraft();
|
|
1602
|
-
if (!input.title.trim()) {
|
|
1603
|
-
throw new IssueStickerHeadlessError(
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1648
|
+
if (!((_a = input.title) == null ? void 0 : _a.trim()) && !((_b = input.description) == null ? void 0 : _b.trim()) && !((_c = input.intent) == null ? void 0 : _c.trim())) {
|
|
1649
|
+
throw new IssueStickerHeadlessError(
|
|
1650
|
+
"VALIDATION_ERROR",
|
|
1651
|
+
"제목·설명 또는 기대한 동작(intent)을 입력해주세요."
|
|
1652
|
+
);
|
|
1607
1653
|
}
|
|
1608
1654
|
this.state.setSnapshot({ phase: "submittingIssue", error: null });
|
|
1609
1655
|
try {
|
|
@@ -1615,6 +1661,7 @@ class HeadlessClientImpl {
|
|
|
1615
1661
|
screenshotPreviewUrls,
|
|
1616
1662
|
title: input.title,
|
|
1617
1663
|
description: input.description,
|
|
1664
|
+
intent: input.intent,
|
|
1618
1665
|
dynamicFields: input.dynamicFields ?? {}
|
|
1619
1666
|
};
|
|
1620
1667
|
const createdIssue = await submitHeadlessIssueDraft({
|
|
@@ -1627,7 +1674,7 @@ class HeadlessClientImpl {
|
|
|
1627
1674
|
config: this.config,
|
|
1628
1675
|
projectId: this.state.requireProjectId(),
|
|
1629
1676
|
logs: this.logCollectors.collect(),
|
|
1630
|
-
currentUrl:
|
|
1677
|
+
currentUrl: getCurrentPageUrl(),
|
|
1631
1678
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1632
1679
|
uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
|
|
1633
1680
|
});
|
|
@@ -1647,6 +1694,76 @@ class HeadlessClientImpl {
|
|
|
1647
1694
|
throw nextError;
|
|
1648
1695
|
}
|
|
1649
1696
|
}
|
|
1697
|
+
async createIssue(input) {
|
|
1698
|
+
var _a, _b;
|
|
1699
|
+
try {
|
|
1700
|
+
if (!((_a = input.title) == null ? void 0 : _a.trim()) || !((_b = input.description) == null ? void 0 : _b.trim())) {
|
|
1701
|
+
return this.skipCreateIssue("제목과 설명이 모두 필요합니다.");
|
|
1702
|
+
}
|
|
1703
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
1704
|
+
return this.skipCreateIssue("브라우저 DOM 에서만 이슈를 만들 수 있습니다.");
|
|
1705
|
+
}
|
|
1706
|
+
await this.ensureInitialized();
|
|
1707
|
+
if (this.snapshot.extensionActive) {
|
|
1708
|
+
return this.skipCreateIssue("이슈스티커 익스텐션이 이 페이지를 사용 중입니다.");
|
|
1709
|
+
}
|
|
1710
|
+
if (!this.autoIssueRateLimit.claim()) {
|
|
1711
|
+
return this.skipCreateIssue(
|
|
1712
|
+
`${this.autoIssueRateLimit.label}까지 호출할 수 있습니다. 호출 조건을 다시 확인해주세요.`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
const target = input.target ?? document.body;
|
|
1716
|
+
const sessionReplay = this.sessionReplayController.getSnapshot();
|
|
1717
|
+
const draft = buildHeadlessCaptureContext({
|
|
1718
|
+
target,
|
|
1719
|
+
sessionReplayEnabled: Boolean(sessionReplay.enabled && sessionReplay.canAttach)
|
|
1720
|
+
});
|
|
1721
|
+
const logs = this.logCollectors.collect();
|
|
1722
|
+
const screenshotPreviewUrls = input.media === "none" ? [] : await this.logCollectors.pauseNetworkWhile(
|
|
1723
|
+
() => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
|
|
1724
|
+
);
|
|
1725
|
+
const formSchema = this.snapshot.auth.mode === "member" && Object.keys(input.dynamicFields ?? {}).length > 0 ? await this.loadFormSchema({}) : null;
|
|
1726
|
+
const createdIssue = await submitHeadlessIssueDraft({
|
|
1727
|
+
draft: {
|
|
1728
|
+
...draft,
|
|
1729
|
+
screenshotPreviewUrls,
|
|
1730
|
+
title: input.title,
|
|
1731
|
+
description: input.description,
|
|
1732
|
+
dynamicFields: input.dynamicFields ?? {}
|
|
1733
|
+
},
|
|
1734
|
+
pendingVideoFile: null,
|
|
1735
|
+
transport: this.transport,
|
|
1736
|
+
issueRepository: this.issueRepository,
|
|
1737
|
+
formSchema,
|
|
1738
|
+
auth: this.snapshot.auth,
|
|
1739
|
+
config: this.config,
|
|
1740
|
+
projectId: this.state.requireProjectId(),
|
|
1741
|
+
logs,
|
|
1742
|
+
currentUrl: getCurrentPageUrl(),
|
|
1743
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1744
|
+
uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
|
|
1745
|
+
});
|
|
1746
|
+
this.state.setSnapshot({
|
|
1747
|
+
issues: [createdIssue, ...this.snapshot.issues]
|
|
1748
|
+
});
|
|
1749
|
+
return createdIssue;
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
return this.skipCreateIssue("이슈를 생성하지 못했습니다.", error);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* createIssue 가 이슈를 만들지 않고 끝난 이유를 남긴다.
|
|
1756
|
+
*
|
|
1757
|
+
* devLogger 는 프로덕션 빌드에서 침묵하는데, 이 실패는 예외로 드러나지 않으므로
|
|
1758
|
+
* 통합한 쪽이 프로덕션에서도 알아야 한다. 접두어가 붙어 있어 우리 수집기가
|
|
1759
|
+
* 자기 로그로 걸러내므로 고객 이슈의 콘솔 로그를 오염시키지 않는다.
|
|
1760
|
+
*/
|
|
1761
|
+
skipCreateIssue(reason, cause) {
|
|
1762
|
+
const args = [ISSUE_STICKER_INTERNAL_LOG_PREFIX, "[headless] createIssue:", reason];
|
|
1763
|
+
if (cause !== void 0) args.push(cause);
|
|
1764
|
+
console.warn(...args);
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1650
1767
|
async loginAsMember() {
|
|
1651
1768
|
await this.ensureInitialized();
|
|
1652
1769
|
if (!this.authBridge.canUseBridge) {
|
|
@@ -1685,10 +1802,15 @@ class HeadlessClientImpl {
|
|
|
1685
1802
|
this.sessionReplayController.destroy();
|
|
1686
1803
|
this.clearPendingMedia();
|
|
1687
1804
|
this.logCollectors.stop();
|
|
1805
|
+
this.autoIssueRateLimit.reset();
|
|
1688
1806
|
this.authBridge.destroy();
|
|
1689
1807
|
this.state.reset();
|
|
1690
1808
|
}
|
|
1691
1809
|
async initializeInternal() {
|
|
1810
|
+
if (isIssueStickerExtensionRuntimeActive()) {
|
|
1811
|
+
this.state.setSnapshot({ extensionActive: true, phase: "idle", error: null });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1692
1814
|
this.state.setSnapshot({ phase: "initializing", error: null });
|
|
1693
1815
|
try {
|
|
1694
1816
|
const result = await this.request("/api/sdk/auth/public-init", {
|
|
@@ -1817,7 +1939,13 @@ class HeadlessClientImpl {
|
|
|
1817
1939
|
const token = {};
|
|
1818
1940
|
this.captureSession = {
|
|
1819
1941
|
token,
|
|
1820
|
-
screenshots: draft.mediaMode === "screenshot" ?
|
|
1942
|
+
screenshots: draft.mediaMode === "screenshot" ? (
|
|
1943
|
+
// 스크린샷은 웹폰트·이미지를 fetch 로 인라인한다. 그 사이 수집을 멈추지 않으면
|
|
1944
|
+
// 최근 50건이 그 요청들로 채워져 진짜 앱 요청이 밀려난다.
|
|
1945
|
+
this.logCollectors.pauseNetworkWhile(
|
|
1946
|
+
() => captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() })
|
|
1947
|
+
)
|
|
1948
|
+
) : null,
|
|
1821
1949
|
formSchema: this.loadFormSchema({})
|
|
1822
1950
|
};
|
|
1823
1951
|
const isCurrentSession = () => {
|