Setup HubSpot chat Integration

Do you want to integrate a CSM on Discourse? Let’s see how to integrate the HubSpot chat on Discourse!

  1. create an account to HubSpot

  2. select Chat

  3. customize the interface and the availability as you wish


  4. Copy the code

  5. Create a new theme component and paste it in the Common - </body> tab. Add the new component to your main theme(s)

  6. Complete the Verification on Hubspot to activate the widget and go to your website

  7. Done :tada:

Last edited by @david 2026-03-19T10:07:30Z

Check documentPerform check on document:
12개의 좋아요

다니엘라님, 안녕하세요. 세 가지 다른 채팅 제공업체와 이 통합을 시도해 보았는데, security policy script src에 찾을 수 있는 모든 링크를 추가해도 동일한 CSP(Cross-Site Scripting) 오류가 계속 발생합니다. 특히 Tidio에서 발생하는 문제인데, LiveChat과 Pure Chat에서도 동일한 현상이 있었습니다. 여기서 무슨 일이 일어나고 있는지 혹시 아이디어가 있으신가요?

Content Security Policy: The page’s settings blocked the loading of a resource at eval (“script-src”). Source: (function injected(eventName, injectedIntoContentWindow)
{
  let checkRequest;

  /*
   * Frame context wrapper
   *
   * For some edge-cases Chrome will not run content scripts inside of frames.
   * Website have started to abuse this fact to access unwrapped APIs via a
   * frame's contentWindow (#4586, 5207). Therefore until Chrome runs content
   * scripts consistently for all frames we must take care to (re)inject our
   * wrappers when the contentWindow is accessed.
   */
  let injectedToString = Function.prototype.toString.bind(injected);
  let injectedFrames = new WeakSet();
  let injectedFramesAdd = WeakSet.prototype.add.bind(injectedFrames);
  let injectedFramesHas = WeakSet.prototype.has.bind(injectedFrames);

  function injectIntoContentWindow(contentWindow)
  {
    if (contentWindow && !injectedFramesHas(contentWindow))
    {
      injectedFramesAdd(contentWindow);
      try
      {
        contentWindow[eventName] = checkRequest;
        contentWindow.eval(
          "(" + injectedToString() + ")('" + eventName + "', true);"
        );
        delete contentWindow[eventName];
      }
      catch (e) {}
    }
  }

  for (let element of [HTMLFrameElement, HTMLIFrameElement, HTMLObjectElement])
  {
    let contentDocumentDesc = Object.getOwnPropertyDescriptor(
      element.prototype, "contentDocument"
    );
    let contentWindowDesc = Object.getOwnPropertyDescriptor(
      element.prototype, "contentWindow"
    );

    // Apparently in HTMLObjectElement.prototype.contentWindow does not exist
    // in older versions of Chrome such as 51.
    if (!contentWindowDesc)
      continue;

    let getContentDocument = Function.prototype.call.bind(
      contentDocumentDesc.get
    );
    let getContentWindow = Function.prototype.call.bind(
      contentWindowDesc.get
    );

    contentWindowDesc.get = function()
    {
      let contentWindow = getContentWindow(this);
      injectIntoContentWindow(contentWindow);
      return contentWindow;
    };
    contentDocumentDesc.get = function()
    {
      injectIntoContentWindow(getContentWindow(this));
      return getContentDocument(this);
    };
    Object.defineProperty(element.prototype, "contentWindow",
                          contentWindowDesc);
    Object.defineProperty(element.prototype, "contentDocument",
                          contentDocumentDesc);
  }

  /*
   * RTCPeerConnection wrapper
   *
   * The webRequest API in Chrome does not yet allow the blocking of
   * WebRTC connections.
   * See https://bugs.chromium.org/p/chromium/issues/detail?id=707683
   */
  let RealCustomEvent = window.CustomEvent;

  // If we've been injected into a frame via contentWindow then we can simply
  // grab the copy of checkRequest left for us by the parent document. Otherwise
  // we need to set it up now, along with the event handling functions.
  if (injectedIntoContentWindow)
    checkRequest = window[eventName];
  else
  {
    let addEventListener = document.addEventListener.bind(document);
    let dispatchEvent = document.dispatchEvent.bind(document);
    let removeEventListener = document.removeEventListener.bind(document);
    checkRequest = (url, callback) =>
    {
      let incomingEventName = eventName + "-" + url;

      function listener(event)
      {
        callback(event.detail);
        removeEventListener(incomingEventName, listener);
      }
      addEventListener(incomingEventName, listener);

      dispatchEvent(new RealCustomEvent(eventName, {detail: {url}}));
    };
  }

  // Only to be called before the page's code, not hardened.
  function copyProperties(src, dest, properties)
  {
    for (let name of properties)
    {
      if (Object.prototype.hasOwnProperty.call(src, name))
      {
        Object.defineProperty(dest, name,
                              Object.getOwnPropertyDescriptor(src, name));
      }
    }
  }

  let RealRTCPeerConnection = window.RTCPeerConnection ||
                              window.webkitRTCPeerConnection;

  // Firefox has the option (media.peerconnection.enabled) to disable WebRTC
  // in which case RealRTCPeerConnection is undefined.
  if (typeof RealRTCPeerConnection != "undefined")
  {
    let closeRTCPeerConnection = Function.prototype.call.bind(
      RealRTCPeerConnection.prototype.close
    );
    let RealArray = Array;
    let RealString = String;
    let {create: createObject, defineProperty} = Object;

    let normalizeUrl = url =>
    {
      if (typeof url != "undefined")
        return RealString(url);
    };

    let safeCopyArray = (originalArray, transform) =>
    {
      if (originalArray == null || typeof originalArray != "object")
        return originalArray;

      let safeArray = RealArray(originalArray.length);
      for (let i = 0; i < safeArray.length; i++)
      {
        defineProperty(safeArray, i, {
          configurable: false, enumerable: false, writable: false,
          value: transform(originalArray[i])
        });
      }
      defineProperty(safeArray, "length", {
        configurable: false, enumerable: false, writable: false,
        value: safeArray.length
      });
      return safeArray;
    };

    // It would be much easier to use the .getConfiguration method to obtain
    // the normalized and safe configuration from the RTCPeerConnection
    // instance. Unfortunately its not implemented as of Chrome unstable 59.
    // See https://www.chromestatus.com/feature/5271355306016768
    let protectConfiguration = configuration =>
    {
      if (configuration == null || typeof configuration != "object")
        return configuration;

      let iceServers = safeCopyArray(
        configuration.iceServers,
        iceServer =>
        {
          let {url, urls} = iceServer;

          // RTCPeerConnection doesn't iterate through pseudo Arrays of urls.
          if (typeof urls != "undefined" && !(urls instanceof RealArray))
            urls = [urls];

          return createObject(iceServer, {
            url: {
              configurable: false, enumerable: false, writable: false,
              value: normalizeUrl(url)
            },
            urls: {
              configurable: false, enumerable: false, writable: false,
              value: safeCopyArray(urls, normalizeUrl)
            }
          });
        }
      );

      return createObject(configuration, {
        iceServers: {
          configurable: false, enumerable: false, writable: false,
          value: iceServers
        }
      });
    };

    let checkUrl = (peerconnection, url) =>
    {
      checkRequest(url, blocked =>
      {
        if (blocked)
        {
          // Calling .close() throws if already closed.
          try
          {
            closeRTCPeerConnection(peerconnection);
          }
          catch (e) {}
        }
      });
    };

    let checkConfiguration = (peerconnection, configuration) =>
    {
      if (configuration && configuration.iceServers)
      {
        for (let i = 0; i < configuration.iceServers.length; i++)
        {
          let iceServer = configuration.iceServers[i];
          if (iceServer)
          {
            if (iceServer.url)
              checkUrl(peerconnection, iceServer.url);

            if (iceServer.urls)
            {
              for (let j = 0; j < iceServer.urls.length; j++)
                checkUrl(peerconnection, iceServer.urls[j]);
            }
          }
        }
      }
    };

    // Chrome unstable (tested with 59) has already implemented
    // setConfiguration, so we need to wrap that if it exists too.
    // https://www.chromestatus.com/feature/5596193748942848
    if (RealRTCPeerConnection.prototype.setConfiguration)
    {
      let realSetConfiguration = Function.prototype.call.bind(
        RealRTCPeerConnection.prototype.setConfiguration
      );

      RealRTCPeerConnection.prototype.setConfiguration = function(configuration)
      {
        configuration = protectConfiguration(configuration);

        // Call the real method first, so that validates the configuration for
        // us. Also we might as well since checkRequest is asynchronous anyway.
        realSetConfiguration(this, configuration);
        checkConfiguration(this, configuration);
      };
    }

    let WrappedRTCPeerConnection = function(...args)
    {
      if (!(this instanceof WrappedRTCPeerConnection))
        return RealRTCPeerConnection();

      let configuration = protectConfiguration(args[0]);

      // Since the old webkitRTCPeerConnection constructor takes an optional
      // second argument we need to take care to pass that through. Necessary
      // for older versions of Chrome such as 51.
      let constraints = undefined;
      if (args.length > 1)
        constraints = args[1];

      let peerconnection = new RealRTCPeerConnection(configuration,
                                                     constraints);
      checkConfiguration(peerconnection, configuration);
      return peerconnection;
    };

    WrappedRTCPeerConnection.prototype = RealRTCPeerConnection.prototype;

    let boundWrappedRTCPeerConnection = WrappedRTCPeerConnection.bind();
    copyProperties(RealRTCPeerConnection, boundWrappedRTCPeerConnection,
                   ["generateCertificate", "name", "prototype"]);
    RealRTCPeerConnection.prototype.constructor = boundWrappedRTCPeerConnection;

    if ("RTCPeerConnection" in window)
      window.RTCPeerConnection = boundWrappedRTCPeerConnection;
    if ("webkitRTCPeerConnection" in window)
      window.webkitRTCPeerConnection = boundWrappedRTCPeerConnection;
  }
})('abp-request-aic05wltexc', true);. 468b7929-46c3-4249-b516-189e58962157:27:22

또 다른 CSP:

Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”). Source: try { if (typeof Navigator.prototype.sendBeacon === 'function') { Navigator.prototype.sendBeacon = function(url, data) { return true; }; } } catch (exception) { console.error(exception); }. script.js:517:22

그리고 하나 더 (여기 링크는 추가해 두었습니다)

Content Security Policy: The page’s settings blocked the loading of a resource at https://widget-v4.tidiochat.com//1_23_3/static/js/widget.a6a6e2b4c2401b7c523f.js (“script-src”). xgahvvrt0kwvb7p6crbxuolt4omnin1u.js:1:12450

해당 URL을 화이트리스트에 추가했나요? 오류 메시지는 일반적으로 어떤 URL을 화이트리스트에 추가해야 하는지 명확하게 알려줍니다.

1개의 좋아요

Jeff, 감사합니다. 제가 너무 문자 그대로 이해해서 https://widget-v4.tidiochat.com//1_23_3/static/js/widget.a6a6e2b4c2401b7c523f.js의 전체 URL을 사용하고 있었네요.

이제 작동합니다. 여전히 첫 번째 CSP 오류가 발생하는데, 정상적으로 작동하는 중이라면 이 오류를 무시해도 안전할까요?

1개의 좋아요

F12 콘솔에서 CSP 오류가 발생하면, 오류에 표시된 도메인을 화이트리스트에 추가해야 합니다.

URL이 없는 첫 번째 항목을 말씀드린 건데요, Chrome에서 프레임 컨텍스트 래퍼 관련 문제를 경고하는 내용입니다. 이상하게도 이 경고는 Firefox에서는 표시되는데 Chrome에서는 나타나지 않아요. 그래서 지금은 잘 작동하는 것 같으니 그냥 무시해도 될 것 같습니다. 도움 주셔서 감사합니다 :+1:

저는 이 특정 채팅을 사용해 본 적이 없습니다.

저는 어제 LiveChat을 테스트했는데 오류 없이 정상적으로 작동했습니다.

2개의 좋아요

여러분 안녕하세요,

훌륭한 가이드 감사합니다! 가이드를 따라 피드백 설문을 추가했는데, HubSpot이 분석 링크(js.hs-analytics/)를 계속 변경해서 화이트리스트에 새 링크를 계속 추가해야 하는 상황이 반복되고 있습니다.

일관되게 작동하는 유일한 해결책은 정책을 아예 비활성화하는 것이었지만, 보안 측면에서 너무 불안정해 보입니다.

어떤 아이디어가 있을까요?

지금 사이트를 보고 있는데, Customize > Themes에서 입력하신 설문 코드를 찾을 수 없습니다.

수정: 이제 수정되었어야 합니다. CSP를 다시 활성화했습니다. 문제가 있으면 알려주세요.

1개의 좋아요

안녕하세요 @Dax,

감사합니다! 어떻게 수정하셨는지, 그리고 문제가 무엇이었는지 알려 주실 수 있을까요?

수정: 와일드카드를 추가하신 걸 확인했습니다. 훌륭하시네요! 와일드카드도 선택지라는 걸 알 수 있도록 해당 박스 아래에 주석을 추가하는 것이 가장 좋다고 생각합니다. 저도 그런 해결책을 생각해 봤는데, 어리석게도 직접 시도해 보지 못했네요.

감사합니다!

1개의 좋아요

궁금한 게 하나 있어요. HubSpot 같은 CRM을 우리 커뮤니티 디스커스(Discourse) 사이트에 연결하는 데 어떤 가치가 있을까요?