Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apple): Add support for iOS #334

Merged
merged 26 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fedfcc2
iOS Support
brustolin Jul 5, 2023
460207b
Merge branch 'master' into feat/apple-support
brustolin Jul 5, 2023
30244d0
Delete launch.json
brustolin Jul 5, 2023
2510c4f
Update CHANGELOG.md
brustolin Jul 5, 2023
b260076
Update bin.ts
brustolin Jul 5, 2023
09c22cf
Update code-tools.ts
brustolin Jul 5, 2023
67c2e9f
Update ChooseIntegration.ts
brustolin Jul 5, 2023
1a1c17e
Apply suggestions from code review
brustolin Jul 6, 2023
c7acc22
some fixes
brustolin Jul 6, 2023
6aca98c
Merge branch 'feat/apple-support' of https://github.com/getsentry/sen…
brustolin Jul 6, 2023
af68bc3
Update apple-wizard.ts
brustolin Jul 6, 2023
819317a
Merge branch 'master' into feat/apple-support
brustolin Jul 6, 2023
db2c85d
Update Constants.ts
brustolin Jul 6, 2023
8b693e5
Merge branch 'feat/apple-support' of https://github.com/getsentry/sen…
brustolin Jul 6, 2023
f0303d5
Apply suggestions from code review
brustolin Jul 6, 2023
5ed26db
Merge branch 'master' into feat/apple-support
brustolin Jul 7, 2023
24d6664
multiple projects in the same folder
brustolin Jul 7, 2023
12aefe2
Merge branch 'feat/apple-support' of https://github.com/getsentry/sen…
brustolin Jul 7, 2023
1c754f0
Update CHANGELOG.md
brustolin Jul 7, 2023
ae9bac2
Update xcode-manager.ts
brustolin Jul 7, 2023
f03f50a
Update xcode-manager.ts
brustolin Jul 7, 2023
c9ca7c8
telemetry
brustolin Jul 10, 2023
b70a26e
Update Wizard.ts
brustolin Jul 10, 2023
44b8bde
Update Wizard.ts
brustolin Jul 10, 2023
792e6aa
Apply suggestions from code review
brustolin Jul 10, 2023
efbdd1c
Merge branch 'master' into feat/apple-support
brustolin Jul 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- feat: Add support for iOS (#334)

## 3.5.0

- feat(sourcemaps): Check if correct SDK version is installed (#336)
Expand Down
5 changes: 5 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { runNextjsWizard } from './src/nextjs/nextjs-wizard';
import { runSourcemapsWizard } from './src/sourcemaps/sourcemaps-wizard';
import { runSvelteKitWizard } from './src/sveltekit/sveltekit-wizard';
import { runAppleWizard } from './src/apple/apple-wizard';
import { withTelemetry } from './src/telemetry';
import { WizardOptions } from './src/utils/types';
export * from './lib/Setup';
Expand Down Expand Up @@ -66,11 +67,11 @@
// Collect argv options that are relevant for the new wizard
// flows based on `clack`
const wizardOptions: WizardOptions = {
url: argv.url as string | undefined,

Check warning on line 70 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .url on an `any` value
promoCode: argv['promo-code'] as string | undefined,

Check warning on line 71 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access ['promo-code'] on an `any` value
};

switch (argv.i) {

Check warning on line 74 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access .i on an `any` value
case 'nextjs':
// eslint-disable-next-line no-console
runNextjsWizard(wizardOptions).catch(console.error);
Expand All @@ -82,13 +83,17 @@
case 'sourcemaps':
withTelemetry(
{
enabled: !argv['disable-telemetry'],

Check warning on line 86 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe member access ['disable-telemetry'] on an `any` value
integration: 'sourcemaps',
},
() => runSourcemapsWizard(wizardOptions),
// eslint-disable-next-line no-console
).catch(console.error);
break;
case 'ios':
// eslint-disable-next-line no-console
runAppleWizard(wizardOptions).catch(console.error);
break
default:
void run(argv);
}
5 changes: 5 additions & 0 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Key value should be the same here */
export enum Integration {
reactNative = 'reactNative',
ios = 'ios',
cordova = 'cordova',
electron = 'electron',
nextjs = 'nextjs',
Expand All @@ -14,7 +15,7 @@
android = 'android',
}

export function getPlatformChoices(): any[] {

Check warning on line 18 in lib/Constants.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
return Object.keys(Platform).map((platform: string) => ({
checked: true,
name: getPlatformDescription(platform),
Expand Down Expand Up @@ -45,6 +46,8 @@
return 'SvelteKit';
case Integration.sourcemaps:
return 'Configure Source Maps Upload';
case Integration.ios:
return 'iOS';
default:
return 'React Native';
}
Expand All @@ -64,6 +67,8 @@
return 'javascript-sveltekit';
case Integration.sourcemaps:
return undefined;
case Integration.ios:
return 'iOS';
default:
throw new Error(`Unknown integration ${type}`);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/Steps/ChooseIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Electron } from './Integrations/Electron';
import { NextJsShim } from './Integrations/NextJsShim';
import { ReactNative } from './Integrations/ReactNative';
import { SourceMapsShim } from './Integrations/SourceMapsShim';
import { Apple } from './Integrations/Apple';
import { SvelteKitShim } from './Integrations/SvelteKitShim';

let projectPackage: any = {};
Expand Down Expand Up @@ -51,6 +52,9 @@ export class ChooseIntegration extends BaseStep {
case Integration.sourcemaps:
integration = new SourceMapsShim(this._argv);
break;
case Integration.ios:
integration = new Apple(this._argv);
break;
case Integration.reactNative:
default:
integration = new ReactNative(sanitizeUrl(this._argv));
Expand Down
19 changes: 19 additions & 0 deletions lib/Steps/Integrations/Apple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Answers } from 'inquirer';
import type { Args } from '../../Constants';
import { BaseIntegration } from './BaseIntegration';
import { runAppleWizard } from '../../../src/apple/apple-wizard';

export class Apple extends BaseIntegration {
public constructor(protected _argv: Args) {
super(_argv);
}

public async emit(_answers: Answers): Promise<Answers> {
await runAppleWizard({ promoCode: this._argv.promoCode, url: this._argv.url, });
return {};
}

public async shouldConfigure(_answers: Answers): Promise<Answers> {
return this._shouldConfigure;
}
}
102 changes: 102 additions & 0 deletions src/apple/apple-wizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unused-vars */
// @ts-ignore - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import * as fs from 'fs';
import * as path from 'path';
import * as xcManager from './xcode-manager';
import * as codeTools from './code-tools';
import * as bash from '../utils/bash';
import { WizardOptions } from '../utils/types';

const xcode = require('xcode');
/* eslint-enable @typescript-eslint/no-unused-vars */

import {
askForProjectSelection,
askForSelfHosted,
askForWizardLogin,
askToInstallSentryCLI,
SentryProjectData,
printWelcome,
abort,
askForItemSelection,
} from '../utils/clack-utils';

export async function runAppleWizard(
options: WizardOptions,
): Promise<void> {
printWelcome({
wizardName: 'Sentry Apple Wizard',
promoCode: options.promoCode,
});


if (!bash.hasSentryCLI()) {
if (!(await askToInstallSentryCLI())) {
clack.log.warn("Without sentry-cli, you won't be able to upload debug symbols to Sentry. You can install it later by following the instructions at https://docs.sentry.io/cli/");
} else {
await bash.installSentryCLI();
}
}

const projectDir = process.cwd();
const xcodeProjFiles = findFilesWithExtension(projectDir, ".xcodeproj");

if (!xcodeProjFiles || xcodeProjFiles.length === 0) {
clack.log.error('No xcode project found. Please run this command from the root of your project.');
brustolin marked this conversation as resolved.
Show resolved Hide resolved
brustolin marked this conversation as resolved.
Show resolved Hide resolved
await abort();
return;
brustolin marked this conversation as resolved.
Show resolved Hide resolved
}

let xcodeProjFile;

if (xcodeProjFiles.length === 1) {
xcodeProjFile = xcodeProjFiles[0];
} else {
xcodeProjFile = (await askForItemSelection(xcodeProjFiles, "Which project do you want to add Sentry?")).value;
}

const pbxproj = path.join(projectDir, xcodeProjFile, "project.pbxproj");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to sanity-check (for someone who has no idea about xcode/ios apps): Is this a valid path?

wizard/wizard-apple/test.xcodeproj/project.pbxproj

Background: I briefly tried the wizard locally but couldn't get beyond the pbxproj step:

image

Copy link
Contributor Author

@brustolin brustolin Jul 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The ".xcodeproj" is actually a folder that keeps project configuration files.

I think we should not show the "Successfully..." message in this case.

I briefly tried the wizard locally but couldn't get beyond the pbxproj step:

You just tried to run it, or you really have an xcode project to test it? Because it should have worked if you have a project.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The ".xcodeproj" is actually a folder that keeps project configuration files.

Okay, all good then! I guess I was just confused by the xcodeProjectFile variable then 😅

You just tried to run it

Yeah, but not very methodically and without an actual xcode project. Just wanted to get over a few steps so I created some empty files 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brustolin do you know a repo which would be a example app to test this stuff with ourselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brustolin marked this conversation as resolved.
Show resolved Hide resolved
if (!fs.existsSync(pbxproj)) {
clack.log.error(`No pbxproj found at ${pbxproj}`);
await abort();
return;
}

const { project, apiKey } = await getSentryProjectAndApiKey(options.promoCode, options.url);

xcManager.updateXcodeProject(pbxproj, project, apiKey, true, true);

const projSource = path.join(projectDir, xcodeProjFile.replace(".xcodeproj", ""));
const codeAdded = codeTools.addCodeSnippetToProject(projSource, project.keys[0].dsn.public);
if (!codeAdded) {
clack.log.warn('Added the Sentry dependency to your project but could not add the Sentry code snippet. Please add the code snipped manually by following the docs: https://docs.sentry.io/platforms/apple/guides/ios/#configure');
return;
}

clack.log.success('Sentry was successfully added to your project!');
}

//Prompt for Sentry project and API key
async function getSentryProjectAndApiKey(promoCode?: string, url?: string): Promise<{ project: SentryProjectData, apiKey: { token: string } }> {
const { url: sentryUrl } = await askForSelfHosted(url);

const { projects, apiKeys } = await askForWizardLogin({
promoCode: promoCode,
url: sentryUrl,
platform: 'javascript-nextjs',
brustolin marked this conversation as resolved.
Show resolved Hide resolved
});

const selectedProject = await askForProjectSelection(projects);
return { project: selectedProject, apiKey: apiKeys };
}

//find files with the given extension
function findFilesWithExtension(dir: string, extension: string): string[] {
const files = fs.readdirSync(dir);
return files.filter(file => file.endsWith(extension));
}
77 changes: 77 additions & 0 deletions src/apple/code-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as fs from 'fs';
import * as path from 'path';
import * as templates from './templates';

const swiftAppLaunchRegex = /(func\s+application\s*\(_\sapplication:[^,]+,\s*didFinishLaunchingWithOptions[^,]+:[^)]+\)\s+->\s+Bool\s+{)|(init\s*\([^)]*\)\s*{)/im;
const objcAppLaunchRegex = /-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\)\s*application\s+didFinishLaunchingWithOptions:\s*\(NSDictionary\s*\*\)\s*launchOptions\s*{/im;


function isAppDelegateFile(filePath: string): boolean {
const appLaunchRegex = filePath.toLowerCase().endsWith(".swift") ? swiftAppLaunchRegex : objcAppLaunchRegex;

const fileContent = fs.readFileSync(filePath, 'utf8');
return appLaunchRegex.test(fileContent) || /struct[^:]+:\s*App\s*{/.test(fileContent);
}

function findAppDidFinishLaunchingWithOptions(dir: string): string | null {
const files = fs.readdirSync(dir);
//iterate over subdirectories later,
//the appdelegate usually is in the top level
const dirs: string[] = [];

for (const file of files) {
const filePath = path.join(dir, file);
if (file.endsWith(".swift") || file.endsWith(".m") || file.endsWith(".mm")) {
if (isAppDelegateFile(filePath)) {
return filePath;
}
} else if (!file.startsWith(".") && !file.endsWith(".xcodeproj") && !file.endsWith(".xcassets") && fs.lstatSync(filePath).isDirectory()) {
dirs.push(file);
}
}

for (const dr of dirs) {
const result = findAppDidFinishLaunchingWithOptions(path.join(dir, dr));
if (result) return result;
}
return null;
}

export function addCodeSnippetToProject(projPath: string, dsn: string): boolean {
const appDelegate = findAppDidFinishLaunchingWithOptions(projPath);
if (!appDelegate) {
return false;
}

const fileContent = fs.readFileSync(appDelegate, 'utf8');
const isSwift = appDelegate.toLowerCase().endsWith(".swift");
const appLaunchRegex = isSwift ? swiftAppLaunchRegex : objcAppLaunchRegex;
const importStatement = isSwift ? "import Sentry\n" : "@import Sentry;\n";
const checkForSentryInit = isSwift ? "SentrySDK.start" : "[SentrySDK start";
let codeSnippet = isSwift ? templates.getSwiftSnippet(dsn) : templates.getObjcSnippet(dsn);

if (fileContent.includes(checkForSentryInit)) {
//already initialized
return true;
}

let match = appLaunchRegex.exec(fileContent);
if (!match) {
const swiftUIMatch = /struct[^:]+:\s*App\s*{/.exec(fileContent)
if (!swiftUIMatch) {
return false;
}
//Is SwiftUI with no init
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
match = swiftUIMatch;
codeSnippet = ` init() {\n${codeSnippet} }`;
}

const insertIndex = match.index + match[0].length;
const newFileContent = (fileContent.indexOf(importStatement) >= 0 ? "" : importStatement) +
fileContent.slice(0, insertIndex) + "\n" +
codeSnippet +
fileContent.slice(insertIndex);
fs.writeFileSync(appDelegate, newFileContent, 'utf8');

return true;
}
39 changes: 39 additions & 0 deletions src/apple/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export function getRunScriptTemplate(
orgSlug: string,
projectSlug: string,
apiKey: string,
uploadSource = true,
): string {
// eslint-disable-next-line no-useless-escape
return `# This script is responsable to upload debug symbols and source context for Sentry.\\nif which sentry-cli >/dev/null; then\\nexport SENTRY_ORG=${orgSlug}\\nexport SENTRY_PROJECT=${projectSlug}\\nexport SENTRY_AUTH_TOKEN=${apiKey}\\nERROR=$(sentry-cli debug-files upload ${uploadSource ? "--include-sources " : ""}\"$DWARF_DSYM_FOLDER_PATH\" 2>&1 >/dev/null)\\nif [ ! $? -eq 0 ]; then\\necho \"warning: sentry-cli - $ERROR\"\\nfi\\nelse\\necho \"warning: sentry-cli not installed, download from https://github.com/getsentry/sentry-cli/releases\"\\nfi\\n`;
}

export const scriptInputPath = "\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}\"";

export function getSwiftSnippet(dsn: string): string {
return ` SentrySDK.start { options in
options.dsn = "${dsn}"
options.debug = true // Enabled debug when first installing is always helpful
options.enableTracing = true

//Uncomment the following lines to add more data to your events
//options.attachScreenshot = true //This will add a screenshot to the error events
//options.attachViewHierarchy = true //This will add the view hierarchy to the error events
}
//Remove the next line after confirming that your Sentry integration is working.
brustolin marked this conversation as resolved.
Show resolved Hide resolved
SentrySDK.capture(message: "This app uses Sentry! :)")\n`;
}

export function getObjcSnippet(dsn: string): string {
return ` [SentrySDK startWithConfigureOptions:^(SentryOptions * options) {
options.dsn = @"${dsn}";
options.debug = YES; // Enabled debug when first installing is always helpful
options.enableTracing = YES;

//Uncomment the following lines to add more data to your events
//options.attachScreenshot = YES; //This will add a screenshot to the error events
//options.attachViewHierarchy = YES; //This will add the view hierarchy to the error events
}];
//Remove the next line after confirming that your Sentry integration is working.
[SentrySDK captureMessage:@"This app uses Sentry!"];\n`;
}
Loading
Loading